diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c1f1607..80f1c1c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
### Features
+- feat: |Frontend| 前端新增 6 国语言支持(`zh` / `en` / `es` / `pt-BR` / `ja` / `de`),默认语言保持为 `zh`;无 locale 前缀路由(如 `/`、`/user`)默认使用中文渲染,同时会记录浏览器语言作为语言偏好。用户手动切换后会持久化语言偏好,并保持当前页面路径、查询参数与 canonical locale URL 一致
- feat: |API| 新增服务端解析邮件接口 `/api/parsed_mails` 与 `/api/parsed_mail/:id`,直接返回 `sender` / `subject` / `text` / `html` / `attachments` 元信息(复用 `commonParseMail`),AI agent 侧不再需要引入 MIME 解析器
- feat: |Skill| 新增仓库内置只读 skill `cf-temp-mail-agent-mail`(`skills/cf-temp-mail-agent-mail/`),让 OpenClaw / Codex / Cursor 等 AI agent 凭用户提供的 Address JWT + API 地址读取邮箱、轮询验证码,绕开创建邮箱时的 Turnstile 人机验证;可通过 `npx degit dreamhunter2333/cloudflare_temp_email/skills/cf-temp-mail-agent-mail` 安装
- docs: |文档| 新增"AI Agent 使用邮箱"文档(`guide/feature/agent-email`),说明 `parsed_mail` API 用法,并在 parsed API 不可用时给出对齐前端的 `mail-parser-wasm` + `postal-mime` 本地解析回退方案
diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md
index 841781f5..a8309666 100644
--- a/CHANGELOG_EN.md
+++ b/CHANGELOG_EN.md
@@ -10,6 +10,7 @@
### Features
+- feat: |Frontend| Add six-language frontend support (`zh` / `en` / `es` / `pt-BR` / `ja` / `de`), keep `zh` as the default locale; locale-unprefixed routes (for example `/` and `/user`) render in Chinese by default while still recording browser language as the stored preference. Explicit locale switches are persisted, and the current route, query string, and canonical locale URL stay in sync during switching
- feat: |API| Add server-side parsed-mail endpoints `/api/parsed_mails` and `/api/parsed_mail/:id` that return `sender` / `subject` / `text` / `html` / `attachments` metadata directly (reuses `commonParseMail`), so AI agents no longer need a client-side MIME parser
- feat: |Skill| Bundle a read-only skill `cf-temp-mail-agent-mail` (`skills/cf-temp-mail-agent-mail/`) so AI agents like OpenClaw / Codex / Cursor can consume a mailbox with a user-supplied Address JWT + API base URL — list mails, poll verification codes, etc. — sidestepping the Turnstile challenge required to create a mailbox. Install via `npx degit dreamhunter2333/cloudflare_temp_email/skills/cf-temp-mail-agent-mail`
- docs: |Docs| Add "AI Agent Mailbox Usage" doc (`guide/feature/agent-email`) covering the `parsed_mail` API and a local-parse fallback using `mail-parser-wasm` + `postal-mime` (mirrors the frontend) when parsed endpoints are unavailable
diff --git a/e2e/tests/browser/locale-switch.spec.ts b/e2e/tests/browser/locale-switch.spec.ts
new file mode 100644
index 00000000..5e533067
--- /dev/null
+++ b/e2e/tests/browser/locale-switch.spec.ts
@@ -0,0 +1,64 @@
+import { expect, test } from '@playwright/test';
+import type { Locator, Page } from '@playwright/test';
+
+import { FRONTEND_URL } from '../../fixtures/test-helpers';
+
+const installLocaleInitScript = async (page: Page, locales: string[], preferredLocale: string | null = null) => {
+ await page.addInitScript(
+ ({ locales: initialLocales, preferredLocale: initialPreferredLocale }: { locales: string[]; preferredLocale: string | null }) => {
+ const localeInitStorageKey = '__localeInitDone';
+ if (!window.sessionStorage.getItem(localeInitStorageKey)) {
+ window.localStorage.removeItem('preferredLocale');
+ if (initialPreferredLocale) {
+ window.localStorage.setItem('preferredLocale', initialPreferredLocale);
+ }
+ window.sessionStorage.setItem(localeInitStorageKey, '1');
+ }
+
+ Object.defineProperty(window.navigator, 'language', {
+ configurable: true,
+ get: () => initialLocales[0],
+ });
+ Object.defineProperty(window.navigator, 'languages', {
+ configurable: true,
+ get: () => initialLocales,
+ });
+ },
+ { locales, preferredLocale },
+ );
+};
+
+const selectLanguage = async (page: Page, selectTrigger: Locator, optionLabel: string) => {
+ await selectTrigger.click();
+ const option = page.locator('.n-dropdown-option, .n-dropdown-option-body').filter({ hasText: optionLabel }).first();
+ await expect(option).toBeVisible();
+ await option.click();
+};
+
+test.describe('Locale switching', () => {
+ test('keeps default route in Chinese while persisting browser language preference', async ({ page }) => {
+ await installLocaleInitScript(page, ['es-ES', 'en-US']);
+
+ await page.goto(`${FRONTEND_URL}/`);
+
+ await expect(page).toHaveURL(`${FRONTEND_URL}/`);
+ await expect.poll(() => page.evaluate(() => window.localStorage.getItem('preferredLocale'))).toBe('es');
+ await expect.poll(() => page.evaluate(() => document.documentElement.lang)).toBe('zh');
+ });
+
+ test('mobile drawer switch updates locale route and persisted preference', async ({ page }) => {
+ await page.setViewportSize({ width: 375, height: 844 });
+ await installLocaleInitScript(page, ['zh-CN']);
+
+ await page.goto(`${FRONTEND_URL}/`);
+
+ await page.getByRole('button', { name: /菜单|Menu/i }).click();
+
+ const drawerLocaleDropdown = page.locator('.n-drawer').getByRole('button', { name: /中文|English|Español|Português|日本語|Deutsch/ }).first();
+ await selectLanguage(page, drawerLocaleDropdown, 'Deutsch');
+
+ await expect(page).toHaveURL(`${FRONTEND_URL}/de/`);
+ await expect.poll(() => page.evaluate(() => window.localStorage.getItem('preferredLocale'))).toBe('de');
+ await expect.poll(() => page.evaluate(() => document.documentElement.lang)).toBe('de');
+ });
+});
diff --git a/frontend/package.json b/frontend/package.json
index 3d470d98..48ddd73f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -18,7 +18,7 @@
"deploy:preview": "npm run build && wrangler pages deploy ./dist --branch preview",
"deploy": "npm run build && wrangler pages deploy ./dist --branch production",
"deploy:actions": "npm run build && wrangler pages deploy ./dist",
- "test": "vitest run",
+ "test": "vitest run --passWithNoTests",
"test:watch": "vitest"
},
"dependencies": {
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index ddbd3f0c..0add26c4 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,6 +1,8 @@
-
+
diff --git a/frontend/src/components/AddressSelect.vue b/frontend/src/components/AddressSelect.vue
index 756ae9f6..b890b41f 100644
--- a/frontend/src/components/AddressSelect.vue
+++ b/frontend/src/components/AddressSelect.vue
@@ -1,7 +1,7 @@
@@ -73,7 +73,7 @@ onMounted(() => {
-
+
{{ t('refresh') }}
diff --git a/frontend/src/components/WebhookComponent.vue b/frontend/src/components/WebhookComponent.vue
index c69b0a7d..1bdf4c5a 100644
--- a/frontend/src/components/WebhookComponent.vue
+++ b/frontend/src/components/WebhookComponent.vue
@@ -1,6 +1,6 @@
World
',
- text: '',
- }
- const result = buildReplyModel(mail, 'Reply')
- expect(result.content).not.toContain('',
- text: '',
- }
- const result = buildForwardModel(mail, 'Forward')
- expect(result.content).not.toContain('
diff --git a/frontend/src/views/Header.vue b/frontend/src/views/Header.vue
index a4c5da11..6a92e448 100644
--- a/frontend/src/views/Header.vue
+++ b/frontend/src/views/Header.vue
@@ -1,26 +1,30 @@
diff --git a/frontend/src/views/admin/Account.vue b/frontend/src/views/admin/Account.vue
index 9047faa8..8f2f69cb 100644
--- a/frontend/src/views/admin/Account.vue
+++ b/frontend/src/views/admin/Account.vue
@@ -1,7 +1,7 @@
diff --git a/frontend/src/views/common/Appearance.vue b/frontend/src/views/common/Appearance.vue
index c4650d52..464bd94a 100644
--- a/frontend/src/views/common/Appearance.vue
+++ b/frontend/src/views/common/Appearance.vue
@@ -1,5 +1,5 @@
diff --git a/frontend/src/views/common/Login.vue b/frontend/src/views/common/Login.vue
index 3465c9d6..83c1e9a0 100644
--- a/frontend/src/views/common/Login.vue
+++ b/frontend/src/views/common/Login.vue
@@ -1,6 +1,6 @@