dyad Documentation

Local, open-source AI app builder for power users โœจ v0 / Lovable / Replit / Bolt alternative ๐ŸŒŸ Star if you like it!

RAW Doc

File: README.md

Dyad

Dyad is a local, open-source AI app builder. It's fast, private, and fully under your control โ€” like Lovable, v0, or Bolt, but running right on your machine.

More info at: https://dyad.sh/

๐Ÿš€ Features

  • โšก๏ธ Local: Fast, private and no lock-in.
  • ๐Ÿ›  Bring your own keys: Use your own AI API keys โ€” no vendor lock-in.
  • ๐Ÿ–ฅ๏ธ Cross-platform: Easy to run on Mac or Windows.

๐Ÿ“ฆ Download

No sign-up required. Just download and go.

[๐Ÿ‘‰ Download for your platform](https://www.dyad.sh/#download)

๐Ÿค Community

Join our growing community of AI app builders on Reddit: r/dyadbuilders - share your projects and get help from the community!

๐Ÿ› ๏ธ Contributing

Dyad is open-source (see License info below).

If you're interested in contributing to dyad, please read our contributing doc.

License


File: docs/adrs/0001-host-capability-interface.md

ADR-0001: Host Capability Interface

  • Status: Proposed
  • Date: 2026-02-15
  • Owners: Platform Core
  • Related plan: plans/desktop-mobile-web-unification.md

Context

Dyad currently routes privileged actions through Electron IPC (src/preload.ts, src/ipc/types/*, src/ipc/handlers/*). This tightly couples product logic to desktop-only primitives:

  • local filesystem access
  • local process execution
  • local git and shell operations
  • desktop-only OS/system APIs

Web and mobile clients cannot reuse this runtime model directly. We need one product core that can run against multiple execution hosts:

  • desktop local host
  • cloud host

Decision

Adopt a host capability interface as the canonical execution boundary for privileged operations.

The shared product core will depend on a HostProvider contract, not directly on Electron IPC channels or HTTP endpoints.

Interface shape

HostProvider exposes capability groups:

  • project: read/write/rename/delete/list/search file operations
  • exec: run/stop commands, stream logs/output
  • git: branch/commit/status/sync operations
  • preview: start/stop/status/getPreviewUrl
  • integration: provider-specific operations (supabase/vercel/neon/mcp)
  • system: optional host/system functions (open external URL, show in folder, clipboard/screenshot)
  • session: session cache/state controls

Each operation must include a standard envelope:

  • workspaceId
  • projectId
  • requestId
  • idempotencyKey
  • actor (user/system/assistant)
  • timestamp

Each operation returns:

  • success payload OR typed error payload
  • correlationId for tracing

Streaming model

Streaming operations must follow a uniform event contract:

  • start
  • chunk
  • end
  • error

Desktop provider maps this to IPC streams; cloud provider maps this to WebSocket/SSE streams.

Capability negotiation

Hosts must declare supported capabilities at runtime (for example supportsProcess, supportsNativeDialogs, supportsShowItemInFolder), and UI/features must gate behavior accordingly.

Consequences

Positive

  • Enables shared domain logic across desktop/web/mobile.
  • Prevents transport-specific logic from leaking into features.
  • Creates deterministic observability across hosts.
  • Simplifies adding future hosts.

Negative

  • Requires incremental refactor of existing IPC handlers and call sites.
  • Adds short-term complexity with compatibility adapters.
  • Requires strict contract/version governance.

Alternatives Considered

A. Keep Electron IPC as primary and build web/mobile translators

Rejected because it preserves desktop coupling and creates brittle emulation layers.

B. Build separate APIs per platform

Rejected because it duplicates business logic and causes long-term behavior drift.

C. Move everything to cloud and remove local mode

Rejected for now because it breaks existing local-first desktop workflows.

Rollout Plan

  1. Introduce interface and adapter layers in shared packages.
  2. Wrap desktop local flows with ElectronLocalHostProvider.
  3. Migrate critical flows first: chat stream, response apply, app run/stop, git core.
  4. Enforce host capability checks in UI.
  5. Add CloudHostProvider for desktop cloud mode, then web/mobile.

Acceptance Criteria

  • Desktop local mode behavior remains functionally equivalent on migrated flows.
  • At least one end-to-end flow runs through both providers with identical domain behavior.
  • Stream contracts are transport-agnostic and versioned.

Open Questions

  1. Should integration-specific capabilities be in integration.* or split into first-class capability groups?
  2. What is the minimum backward compatibility window for provider contract versions?

File: docs/adrs/0002-cloud-runtime-topology.md

ADR-0002: Cloud Runtime Topology

  • Status: Proposed
  • Date: 2026-02-15
  • Owners: Runtime Services
  • Related plan: plans/desktop-mobile-web-unification.md

Context

Web and mobile clients need privileged execution capabilities that currently exist only in Electron main process:

  • filesystem mutation
  • command/process execution
  • git operations
  • preview lifecycle management

These capabilities require a secure multi-tenant backend architecture with strong isolation, streaming support, and auditability.

Decision

Adopt a control-plane + worker-plane topology.

Control plane

Services:

  • api-gateway: external API entry, auth verification, rate limiting
  • workspace-service: workspace/project metadata and permissions
  • operation-orchestrator: validates and queues privileged operations
  • stream-broker: fan-out for operation events and chat/runtime streams
  • audit-service: immutable operation/audit log ingestion

Worker plane

Services:

  • runtime-scheduler: allocates isolated runtime instances
  • runtime-worker: executes filesystem/command/preview operations per project
  • git-worker: executes git operations in isolated workspaces (can be separate or embedded initially)

Data plane

  • relational store for control metadata (workspaces, projects, operations)
  • object storage for snapshots/artifacts/log archives
  • secret vault for credentials (never persisted in plain metadata tables)

Isolation and Security Constraints

  • Strong tenant isolation at runtime instance boundary.
  • Project execution roots are sandboxed per runtime instance.
  • Command execution must run with deny-by-default security policies.
  • Network egress policy controls by workspace/project tier.
  • Every privileged operation must emit an auditable event with actor, scope, and result.

Streaming and Execution Semantics

  • Operations are asynchronous with queued execution where needed.
  • Each operation emits typed lifecycle events: queued, started, chunk, completed, failed.
  • Clients reconnect using correlationId and replay cursor.
  • Idempotency keys prevent duplicate writes on retries.

Region and Availability Strategy

Initial:

  • single region deployment with disaster recovery backups
  • active-passive failover for control services

Follow-up:

  • multi-region runtime placement
  • project region pinning for data residency and latency

Consequences

Positive

  • Enables web/mobile execution with desktop-comparable capabilities.
  • Separates policy/orchestration from execution for safer scaling.
  • Supports consistent observability and auditing.

Negative

  • Operational complexity and infra cost increase.
  • Requires robust SRE, security, and incident response maturity.
  • Cold starts and queue latency can degrade UX if not controlled.

Alternatives Considered

A. Single monolithic runtime service

Rejected because it mixes orchestration and execution concerns, making scaling and security controls harder.

B. Fully serverless per-operation execution only

Rejected because long-lived previews and streaming command output need persistent runtime context.

C. Desktop relay model (browser/mobile tunnel into user desktop)

Rejected for v1 due to reliability, availability, and connectivity constraints.

Rollout Plan

  1. Build minimal control plane and runtime worker for file ops + command execution.
  2. Add stream broker and reliable event replay.
  3. Add preview lifecycle management and runtime pooling.
  4. Add git worker path and integration-specific execution policies.
  5. Introduce multi-region strategy after stable single-region operations.

Acceptance Criteria

  • Cloud project can execute core file and command operations with audited traces.
  • Stream reliability meets defined SLOs under target concurrency.
  • Isolation and security checks pass internal and external reviews.

Open Questions

  1. Should git run in dedicated workers from day one, or inside runtime workers initially?
  2. What runtime class tiers are required for cost/performance segmentation at beta launch?

File: docs/adrs/0003-authentication-authorization-model.md

ADR-0003: Authentication and Authorization Model

  • Status: Proposed
  • Date: 2026-02-15
  • Owners: Platform Security
  • Related plan: plans/desktop-mobile-web-unification.md

Context

A multi-platform Dyad requires remote privileged execution. This introduces security requirements not present in desktop-only local mode:

  • user identity across devices
  • workspace-level access control
  • operation-level authorization and consent
  • secure storage and use of provider secrets
  • end-to-end auditing for sensitive actions

Decision

Adopt an identity-first model using OIDC authentication, workspace RBAC authorization, and policy-gated privileged operations.

Authentication Model

  • Use OIDC/OAuth2 for user authentication across desktop/web/mobile.
  • Use short-lived access tokens and rotatable refresh tokens.
  • Desktop, web, and mobile clients use platform-appropriate secure token storage.
  • Service-to-service communication uses mTLS and signed service identities.

Authorization Model

Workspace RBAC

Base roles:

  • owner
  • admin
  • editor
  • viewer

Permissions are evaluated against:

  • workspace
  • project
  • operation type
  • host capability

Operation policy layer

In addition to RBAC, high-risk operations require policy checks:

  • destructive file deletes
  • destructive SQL
  • force push/history rewrite
  • shell commands outside safe policy

Policy outcomes:

  • allow
  • require user approval
  • deny

Secrets Model

  • Provider credentials stored in centralized secret vault.
  • Secrets encrypted with envelope encryption (KMS-managed keys).
  • Secrets scoped minimally (workspace/project/provider).
  • Runtime workers receive short-lived scoped secret grants, not raw long-lived credentials.
  • Secret access events are fully audited.

Audit and Compliance Requirements

Every privileged operation must log:

  • actor id
  • workspace/project scope
  • operation type
  • policy decision
  • result status
  • correlation id
  • timestamp

Audit logs must be immutable and queryable for incident response.

Consequences

Positive

  • Unified identity and access model across all platforms.
  • Defense-in-depth for privileged cloud operations.
  • Clear compliance and forensic posture.

Negative

  • Higher implementation complexity than simple API key auth.
  • Requires policy engine ownership and ongoing governance.
  • Increased onboarding complexity for workspace/admin concepts.

Alternatives Considered

A. API key only auth for clients

Rejected due to poor revocation, weak identity semantics, and high leakage risk.

B. RBAC only without operation policy layer

Rejected because role permissions alone are too coarse for high-risk operations.

C. Store all secrets client-side and forward on demand

Rejected because it increases exposure and complicates cross-device continuity.

Rollout Plan

  1. Implement OIDC auth + workspace/session primitives.
  2. Introduce baseline RBAC enforcement across API endpoints.
  3. Add operation policy engine for high-impact actions.
  4. Migrate provider secrets to centralized vault and deprecate legacy paths.
  5. Add immutable audit log store and admin audit views.

Acceptance Criteria

  • All cloud API operations require authenticated identity and scoped authorization.
  • High-risk operations enforce policy with approval/deny semantics.
  • Secret access is scoped, short-lived, and auditable.
  • Security testing validates token, RBAC, and policy boundaries.

Open Questions

  1. Do we need custom enterprise SSO/SAML in beta or post-GA?
  2. What is the default approval policy for non-destructive command execution?
  3. What audit retention period is required by target compliance commitments?

File: docs/adrs/README.md

Architecture Decision Records

This directory stores Architecture Decision Records (ADRs) for major technical decisions.

ADR List

Status Definitions

  • Proposed: drafted and under review
  • Accepted: approved and should guide implementation
  • Superseded: replaced by a newer ADR
  • Rejected: considered but not adopted

File: docs/agent_architecture.md

Agent Architecture

Previously, Dyad used a pseudo tool-calling strategy using custom XML instead of model's formal tool calling capabilities. Now that models have gotten much better with tool calling, particularly with parallel tool calling, it's beneficial to use a more standard tool calling approach which will also make it much easier to add new tools.

  • The heart of the local agent is in src/pro/main/ipc/handlers/local_agent/local_agent_handler.ts which contains the core agent loop: which keeps calling the LLM until it chooses not to do a tool call or hits the maximum number of steps for the turn.
  • src/pro/main/ipc/handlers/local_agent/tool_definitions.ts contains the list of all the tools available to the Dyad local agent.

Add a tool

If you want to add a new tool, you will want to create a new tool in the src/pro/main/ipc/handlers/local_agent/tools directory. You can look at the existing tools as examples.

Then, import the tool and include it in src/pro/main/ipc/handlers/local_agent/tool_definitions.ts

Finally, you will need to define how to render the custom XML tag (e.g. <dyad-$foo-tool-name>) inside src/components/chat/DyadMarkdownParser.tsx which will typically involve creating a new React component to render the custom XML tag.

Testing

You can add an E2E test by looking at the existing local agent E2E tests which are named like e2e-tests/local_agent*.spec.ts

You can define a tool call testing fixture at e2e-tests/fixtures/engine which allows you to simulate a tool call.


File: docs/architecture.md

Dyad Architecture

This doc describes how the Dyad desktop app works at a high-level. If something is out of date, please feel free to suggest a change via a pull request.

Overview

Dyad is an Electron app that is a local, open-source alternative to AI app builders like Lovable, v0, and Bolt. While the specifics of how other AI app builders are constructed aren't publicly documented, there is available information like system prompts about these other app builders.

Electron Architecture

If you're not familiar with Electron apps, they are similar to a full-stack JavaScript app where there's a client-side called the renderer process which executes the UI code like React and then there's a Node.js process called the main process which is comparable to the server-side portion of a full-stack app. The main process is privileged, meaning it has access to the filesystem and other system resources, whereas the renderer process is sandboxed. The renderer process can communicate to the main process using IPCs which is similar to how the browser communicates to the server using HTTP requests.

Life of a request

The core workflow of Dyad is that a user sends a prompt to the AI which edits the code and is reflected in the preview. We'll break this down step-by-step.

  1. Constructing an LLM request - the LLM request that Dyad sends consists of much more than the prompt (i.e. user input). It includes, by default, the entire codebase as well as a detailed system prompt which gives the LLM instructions to respond in a specific XML-like format (e.g. <dyad-write path="path/to/file.ts">console.log("hi")</dyad-write>).
  2. Stream the LLM response to the UI - It's important to provide visual feedback to the user otherwise they're waiting for several minutes without knowing what's happening so we stream the LLM response and show the LLM response. We have a specialized Markdown parser which parses these <dyad-*> tags like the <dyad-write> tag shown earlier, so we can display the LLM output in a nice UI rather than just printing out raw XML-like text.
  3. Process the LLM response - Once the LLM response has finished, and the user has approved the changes, the response processor in the main process applies these changes. Essentially each <dyad-*> tag described in the system prompt maps to specific logic in the response processor, e.g. writing a file, deleting a file, adding a new NPM package, etc.

To recap, Dyad essentially tells the LLM about a bunch of tools like writing files using the <dyad-*> tags, the renderer process displays these Dyad tags in a nice UI and the main process executes these Dyad tags to apply the changes.

FAQ

Why not use actual tool calls?

One thing that may seem strange is that we don't use actual function calling/tool calling capabilities of the AI and instead use these XML-like syntax which simulate tool calling. This is something I observed from studying the system prompts of other app builders.

I think the two main reasons to use this XML-like format instead of actual tool calling is that:

  1. You can call many tools at once, although some models allow parallel calls, many don't.
  2. There's also evidence that forcing LLMs to return code in JSON (which is essentially what tool calling would entail here) negatively affects the quality.

However, many AI editors do heavily rely on tool calling and this is something that we're evaluating, particularly with upcoming MCP support.

Why isn't Dyad more agentic?

Many other systems (e.g. Cursor) are much more agentic than Dyad. For example, they will call many tools and do things like create a plan, use command-line tools to search through the codebase, run linters and tests and automatically fix the code based on those output.

Dyad, on the other hand, has a relatively simple agentic loop. We will fix TypeScript compiler errors if Auto-fix problems is enabled, but otherwise it's usually a single request to the AI.

The biggest issue with complex agentic workflows is that they can get very expensive very quickly! It's not uncommon to see users report spending a few dollars with a single request because under the hood, that single user requests turns into dozens of LLM requests. To keep Dyad as cost-efficient as possible, we've avoided complex agentic workflows at least until the cost of LLMs is more affordable.

Why does Dyad send the entire codebase with each AI request?

Sending the right context to the AI has been rightfully emphasized as important, so much so that the term "context engineering" is now in vogue.

Sending the entire codebase is the simplest approach and quite effective for small codebases. Another approach is for the user to explicitly select the part of the codebase to use as context. This can be done through the select component feature or manual context management.

However, both of these approaches require users to manually select the right files which isn't always practical. Dyad's Smart Context feature essentially uses smaller models to filter out the most important files in the given chat. That said, we are constantly experimenting with new approaches to context selection as it's quite a difficult problem.

One approach that we don't use is a more agentic-style like what Claude Code and Cursor does where it iteratively searches and navigates through a codebase using tool calls. The main reason we don't do this is due to cost (see the above question: Why isn't Dyad more agentic).


File: docs/i18n.md

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

text
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

text
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 (
    
      
        {t("agent.toolPermissions")}
      
      <SelectItem value="ask">{t("agent.permissionOption.ask")}</SelectItem>
      <SelectItem value="always">
        {t("agent.permissionOption.always")}
      </SelectItem>
    
  );
}

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";


  Welcome to **Dyad**
;

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

File: packages/@dyad-sh/react-vite-component-tagger/README.md

@dyad-sh/react-vite-component-tagger

A Vite plugin that automatically adds data-dyad-id and data-dyad-name attributes to your React components. This is useful for identifying components in the DOM, for example for testing or analytics.

Installation

bash
npm install @dyad-sh/react-vite-component-tagger
# or
yarn add @dyad-sh/react-vite-component-tagger
# or
pnpm add @dyad-sh/react-vite-component-tagger

Usage

Add the plugin to your vite.config.ts file:

ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import dyadTagger from "@dyad-sh/react-vite-component-tagger";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), dyadTagger()],
});

The plugin will automatically add data-dyad-id and data-dyad-name to all your React components.

The data-dyad-id will be a unique identifier for each component instance, in the format path/to/file.tsx:line:column.

The data-dyad-name will be the name of the component.

Testing & Publishing

Bump it to an alpha version and test in Dyad app, eg. "version": "0.0.1-alpha.0",

Then publish it:

sh
cd packages/@dyad-sh/react-vite-component-tagger/ && npm run prepublishOnly && npm publish

Update the scaffold like this:

sh
cd scaffold && pnpm remove @dyad-sh/react-vite-component-tagger && pnpm add -D @dyad-sh/react-vite-component-tagger

Run the E2E tests and make sure it passes.

Then, bump to a normal version, e.g. "0.1.0" and then re-publish. We'll try to match the main Dyad app version where possible.


File: packages/@dyad-sh/nextjs-webpack-component-tagger/README.md

@dyad-sh/nextjs-webpack-component-tagger

A webpack loader for Next.js that automatically adds data-dyad-id and data-dyad-name attributes to your React components. This is useful for identifying components in the DOM, for example for testing or analytics.

Installation

bash
npm install @dyad-sh/nextjs-webpack-component-tagger
# or
yarn add @dyad-sh/nextjs-webpack-component-tagger
# or
pnpm add @dyad-sh/nextjs-webpack-component-tagger

Usage

Add the loader to your next.config.js file:

ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  webpack: (config) => {
    if (process.env.NODE_ENV === "development") {
      config.module.rules.push({
        test: /\.(jsx|tsx)$/,
        exclude: /node_modules/,
        enforce: "pre",
        use: "@dyad-sh/nextjs-webpack-component-tagger",
      });
    }
    return config;
  },
};

export default nextConfig;

The loader will automatically add data-dyad-id and data-dyad-name to all your React components.

The data-dyad-id will be a unique identifier for each component instance, in the format path/to/file.tsx:line:column.

The data-dyad-name will be the name of the component.

Testing & Publishing

Bump it to an alpha version and test in Dyad app, eg. "version": "0.0.1-alpha.0",

Then publish it:

sh
cd packages/@dyad-sh/nextjs-webpack-component-tagger/ && npm run prepublishOnly && npm publish

Update the package version in the nextjs-template repo in your personal fork.

Update the src/shared/templates.ts to use your fork of the next.js template, e.g.

text
githubUrl: "https://github.com/wwwillchen/nextjs-template",

Run the E2E tests and make sure it passes.

Then, bump to a normal version, e.g. "0.1.0" and then re-publish. We'll try to match the main Dyad app version where possible.