Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[**English**](CONTRIBUTING.md) | [**简体中文**](CONTRIBUTING.zh.md)

# Contributing to Browser Use Desktop

Install these first:
Expand Down Expand Up @@ -86,6 +88,39 @@ Please describe both a problem + solution! Include:

If you plan to send a PR for the feature, open an issue first! and tag it in your PR.

## Internationalization (i18n)

The app ships with English and Chinese (简体中文) interfaces. All UI strings use the **key = fallback** pattern — the English text itself serves as the translation key, so missing keys fall back gracefully to English.

### Adding or editing strings

1. Edit the component — wrap user-facing strings with `t('...')` (inside a React component) or `i18n.t('...')` (in module-level code).
2. Add the English key to `app/src/renderer/locales/en.json` (value = key).
3. Add the corresponding translation to `app/src/renderer/locales/zh.json` (or your target locale).
4. Run `cd app && task typecheck` to verify.

### Adding a new locale

1. Create `app/src/renderer/locales/{locale}.json` with all keys from `en.json`.
2. Add the locale option to the language dropdown in `app/src/renderer/hub/SettingsPane.tsx`.
3. Open a PR — one locale per PR please.

### File structure

```
app/src/renderer/
i18n.ts # i18next initialization
locales/
en.json # English source strings
zh.json # Chinese (simplified) translations
```

### Adding i18n to a new file

- **React components:** import `useTranslation` from `react-i18next`, call the `t()` function.
- **Module-level code (outside components):** import `i18n` from the relative path to `i18n.ts` (e.g. `../i18n` or `../../i18n` depending on file depth), call `i18n.t()`.
- All 5 renderer entry points (`hub/`, `onboarding/`, `pill/`, `popup/`, `logs/`) already wrap their content with `<I18nextProvider>`, so any component inside them can use the hook.

## Where to ask questions

[Browser Use Discord](https://discord.com/invite/fqPB2NCNKV)
Expand Down
124 changes: 124 additions & 0 deletions CONTRIBUTING.zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
[**English**](CONTRIBUTING.md) | [**简体中文**](CONTRIBUTING.zh.md)

# 贡献指南

首先安装以下工具:

- [Task](https://taskfile.dev)(macOS: `brew install go-task`)
- Node.js 22
- Yarn

从根目录开始:

```bash
git clone https://github.com/browser-use/desktop.git
cd desktop
task up
```

`task up` 会安装依赖、修补本地 Electron 应用包并启动桌面应用。

常用开发命令:

```bash
task --list # 查看所有可用任务
task lint # 运行 ESLint
task typecheck # 运行 tsc --noEmit
cd app && yarn test # 运行单元测试和集成测试
task make # 构建平台安装包
```

Linux 包在 Docker 中构建:

```bash
task linux:make:docker
```

## Pull Request 规范

优秀的 PR 应聚焦且易于审查:

1. 说明为什么需要这个改动。
2. 将 PR 范围限定在一个 bug 修复、功能或清理上。
3. UI 改动请附带截图或简短录屏。

在 Discord、Twitter 或邮件中联系 Browser Use 团队成员,可加快 PR 审查速度。

## 报告 Bug

在 [browser-use/desktop/issues](https://github.com/browser-use/desktop/issues) 创建 issue,提供足够细节以便他人复现。

良好的 Bug 报告包括:

- 应用版本 / git commit
- 您的操作系统
- 您使用的提供商(如 Claude Code 或 Codex)
- 复现问题的清晰步骤
- 期望结果与实际结果
- Bug 可见时的截图或录屏
- 相关日志(请打码密钥和私有 URL)

有用的日志命令:

```bash
task logs:all
task logs:app
task logs:browser
task logs:agent SESSION_ID=<会话ID>
task logs:engine
task logs:errors
```

默认日志路径:

```text
~/Library/Application Support/Browser Use/logs
```

## 功能请求

请同时描述问题和解决方案!包括:

- 当前应用为何无法很好解决该问题
- 您期望的具体成果
- 截图、录屏或示例网站(如有助说明)

如果您打算为该功能提交 PR,请先创建 issue!并在 PR 中关联该 issue。

## 国际化(i18n)

本应用内置简体中文和英文界面。所有 UI 字符串采用 **key = fallback** 模式——英文原文即翻译键,缺失的键会优雅地回退为英文。

### 添加或编辑字符串

1. 修改组件——用 `t('...')`(React 组件内)或 `i18n.t('...')`(模块级代码)包裹面向用户的字符串。
2. 将英文键添加到 `app/src/renderer/locales/en.json`(值 = 键)。
3. 将对应翻译添加到 `app/src/renderer/locales/zh.json`(或您要添加的语言)。
4. 运行 `cd app && task typecheck` 验证。

### 添加新语言

1. 根据 `en.json` 创建 `app/src/renderer/locales/{语言}.json`,包含所有键。
2. 在 `app/src/renderer/hub/SettingsPane.tsx` 的语言下拉框中添加新语言选项。
3. 提交 PR——每次只添加一种语言。

### 文件结构

```
app/src/renderer/
i18n.ts # i18next 初始化
locales/
en.json # 英文源字符串
zh.json # 简体中文翻译
```

### 在新文件中使用 i18n

- **React 组件:** 从 `react-i18next` 导入 `useTranslation`,调用 `t()` 函数。
- **模块级代码(组件外):** 根据文件深度从相对路径导入 `i18n`(如 `../i18n` 或 `../../i18n`),调用 `i18n.t()`。
- 全部 5 个渲染进程入口(`hub/`、`onboarding/`、`pill/`、`popup/`、`logs/`)已包裹 `<I18nextProvider>`,内部的任何组件均可使用该 hook。

## 提问

[Browser Use Discord](https://discord.com/invite/fqPB2NCNKV)
[Twitter](https://x.com/browser_use)
16 changes: 4 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<img width="1456" height="484" alt="desktop-app-banner" src="https://github.com/user-attachments/assets/550ca16a-5a61-4ded-92f0-a30421870223" />

[**English**](README.md) | [**简体中文**](README.zh.md)

# Browser Use Desktop App

> Run a team of browser agents on your desktop.
Expand Down Expand Up @@ -37,19 +39,9 @@ Inbound message channels can trigger agent sessions automatically.

- **WhatsApp** — text yourself with `@BU` to send and receive agent messages

## Development

Requires [Task](https://taskfile.dev) (`brew install go-task`).

```bash
task up # Install deps and start the app
```

Linux packages are built in Docker so local distro tools do not affect the output:
## Internationalization

```bash
task linux:make:docker
```
This app supports English and 简体中文. Switch language in **Settings → Language**.

## License

Expand Down
48 changes: 48 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<img width="1456" height="484" alt="desktop-app-banner" src="https://github.com/user-attachments/assets/550ca16a-5a61-4ded-92f0-a30421870223" />

[**English**](README.md) | [**简体中文**](README.zh.md)

# Browser Use Desktop App

> 在桌面上运行一组浏览器代理。

其他 AI 浏览器总想把浏览器和代理合二为一。你不用换 Chrome——这个只是代理端。

把你的 Cookie 导入全新的 Chromium,代理就能在你访问过的所有网站自动保持登录。还可以用全局快捷键从任何地方启动任务。

基于 [Browser Harness](https://github.com/browser-use/browser-harness) 构建。

<img width="3542" height="2298" alt="CleanShot 2026-05-01 at 12 18 27@2x" src="https://github.com/user-attachments/assets/edd4f6e0-0efe-4b16-b772-b73d5a1a6d23" />

## 下载

[![下载 macOS 版](https://img.shields.io/badge/Download_for_macOS-000000?style=for-the-badge&logo=apple&logoColor=white)](https://github.com/browser-use/desktop/releases/latest/download/Browser-Use-arm64.dmg)
[![下载 Windows 版](https://img.shields.io/badge/Download_for_Windows-0078D4?style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA4OCA4OCI%2BPHBhdGggZmlsbD0iI2ZmZiIgZD0iTTAgMTIuNCAzNiA3LjV2MzQuOEgwem00MC4zLTUuNUw4OCAwdjQxLjhINDAuM3pNMCA0NS43aDM2djM0LjhMMCA3NS42em00MC4zLjVIODhWODhsLTQ3LjctNi43eiIvPjwvc3ZnPg%3D%3D&logoColor=white)](https://github.com/browser-use/desktop/releases/latest/download/Browser-Use-Setup.exe)
[![下载 Linux 版](https://img.shields.io/badge/Download_for_Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black)](https://github.com/browser-use/desktop/releases/latest/download/Browser-Use-x64.AppImage)

**macOS (Apple Silicon):** [Browser-Use-arm64.dmg](https://github.com/browser-use/desktop/releases/latest/download/Browser-Use-arm64.dmg)

**Windows (x64):** [Browser-Use-Setup.exe](https://github.com/browser-use/desktop/releases/latest/download/Browser-Use-Setup.exe)

**Linux:** [Browser-Use-x64.AppImage](https://github.com/browser-use/desktop/releases/latest/download/Browser-Use-x64.AppImage) 支持应用内自动更新。`.deb` 和 `.rpm` 包也发布在 GitHub Releases 中,可手动安装。

上述按钮和链接始终指向最新版本。

## 提供商

- **Anthropic** - Claude Code 订阅或 API 密钥
- **Codex** - ChatGPT 订阅或 API 密钥

## 通道

消息通道可以自动触发代理会话。

- **WhatsApp** — 给自己发一条带 `@BU` 的消息,即可收发代理消息

## 国际化

本应用支持简体中文和 English 界面。在 **设置 → 语言** 中切换。

## 许可证

MIT
2 changes: 2 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@
"remark-gfm": "^4.0.1",
"ws": "^8.20.0",
"zod": "^4.3.6",
"i18next": "^25.2.0",
"react-i18next": "^15.6.0",
"zustand": "^5.0.13"
}
}
7 changes: 5 additions & 2 deletions app/src/renderer/components/base/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import React, {
useRef,
ReactNode,
} from 'react';
import { useTranslation } from 'react-i18next';
import { createPortal } from 'react-dom';

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -86,6 +87,7 @@ interface ToastEntryProps {
}

function ToastEntry({ item, onDismiss }: ToastEntryProps) {
const { t } = useTranslation();
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const scheduleDismiss = useCallback(() => {
Expand Down Expand Up @@ -129,7 +131,7 @@ function ToastEntry({ item, onDismiss }: ToastEntryProps) {
<button
className={`${BLOCK}__dismiss`}
onClick={() => onDismiss(item.id)}
aria-label="Dismiss notification"
aria-label={t('Dismiss notification')}
>
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">
<path
Expand All @@ -149,6 +151,7 @@ function ToastEntry({ item, onDismiss }: ToastEntryProps) {
// ---------------------------------------------------------------------------

export function ToastProvider({ children }: { children: ReactNode }) {
const { t } = useTranslation();
const [toasts, setToasts] = useState<ToastItem[]>([]);

const show = useCallback((item: Omit<ToastItem, 'id'>): string => {
Expand Down Expand Up @@ -182,7 +185,7 @@ export function ToastProvider({ children }: { children: ReactNode }) {
<div
className={`${BLOCK}__stack`}
role="region"
aria-label="Notifications"
aria-label={t('Notifications')}
aria-live="polite"
>
{toasts.map((item) => (
Expand Down
17 changes: 5 additions & 12 deletions app/src/renderer/components/empty/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,7 @@
*/

import React, { Component, ErrorInfo } from 'react';

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const HEADING_COPY = 'Something broke' as const;
const BODY_COPY = 'An unexpected error occurred. You can try reloading.' as const;
const RELOAD_LABEL = 'Reload' as const;
import i18n from '../../i18n';

// ---------------------------------------------------------------------------
// Types
Expand Down Expand Up @@ -72,16 +65,16 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
}

return (
<div className="empty-state" data-variant="error" role="alert" aria-label={HEADING_COPY}>
<p className="empty-state__heading">{HEADING_COPY}</p>
<p className="empty-state__body">{BODY_COPY}</p>
<div className="empty-state" data-variant="error" role="alert" aria-label={i18n.t('Something broke')}>
<p className="empty-state__heading">{i18n.t('Something broke')}</p>
<p className="empty-state__body">{i18n.t('An unexpected error occurred. You can try reloading.')}</p>

<button
type="button"
className="empty-state__reload-btn"
onClick={this.handleReload}
>
{RELOAD_LABEL}
{i18n.t('Reload')}
</button>
</div>
);
Expand Down
15 changes: 5 additions & 10 deletions app/src/renderer/components/empty/OfflineBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,14 @@
*/

import React, { useState, useEffect, useCallback } from 'react';

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const OFFLINE_MESSAGE = "You're offline. Some features may not work." as const;
const DISMISS_LABEL = 'Dismiss offline banner' as const;
import { useTranslation } from 'react-i18next';

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

export function OfflineBanner(): React.ReactElement | null {
const { t } = useTranslation();
const [isOffline, setIsOffline] = useState<boolean>(!navigator.onLine);
const [dismissed, setDismissed] = useState<boolean>(false);

Expand All @@ -42,7 +37,7 @@ export function OfflineBanner(): React.ReactElement | null {
className="offline-banner"
role="status"
aria-live="polite"
aria-label={OFFLINE_MESSAGE}
aria-label={t("You're offline. Some features may not work.")}
>
{/* Warning icon */}
<svg
Expand All @@ -69,13 +64,13 @@ export function OfflineBanner(): React.ReactElement | null {
<circle cx="7" cy="10.5" r="0.6" fill="currentColor" />
</svg>

<span className="offline-banner__message">{OFFLINE_MESSAGE}</span>
<span className="offline-banner__message">{t("You're offline. Some features may not work.")}</span>

<button
type="button"
className="offline-banner__dismiss"
onClick={() => setDismissed(true)}
aria-label={DISMISS_LABEL}
aria-label={t('Dismiss offline banner')}
>
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">
<path
Expand Down
Loading