From eb62c37e0233643a469b14c2ba4405d6b5dd537d Mon Sep 17 00:00:00 2001
From: bhwa233 <404174262@qq.com>
Date: Sat, 25 Apr 2026 13:46:26 +0800
Subject: [PATCH] feat(i18n): enhance locale handling and routing (#996)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(i18n): enhance locale handling and routing
- Implemented dynamic locale aliases in router configuration.
- Added support for preferred locale storage in global state.
- Improved locale resolution logic in router beforeEach guard.
- Created utility functions for locale management and path manipulation.
- Added tests for locale matching and message extraction.
- Updated Header component to allow language selection.
- Refactored getRouterPathWithLang to utilize new locale utilities.
- Updated Vite configuration to support aliasing for vue-i18n.
- Bumped version numbers across various packages to 1.9.0.
* feat(i18n): update version to 1.8.0 and enhance locale handling
- Updated version numbers across all package.json files to 1.8.0.
- Enhanced locale handling in App.vue by centralizing locale configurations.
- Improved Turnstile component to support dynamic language rendering.
- Refactored i18n utilities to include initial locale setup and empty locale messages.
- Updated i18n.ts to utilize the new locale management structure.
- Added naive-locale.ts for better integration with Naive UI's locale handling.
- Adjusted Header.vue to streamline language selection and locale changes.
- Fixed translations in multiple locale files for consistency and accuracy.
* fix(i18n): address review feedback
* feat(i18n): update default locale to English and enhance language handling in components
* fix(i18n): switch locale selector to dropdown
* docs: add topbar language and github order design spec
* fix(i18n): 修复 Header 语言切换器相关问题,恢复为独立控件并调整样式
* Refactor locale handling in router and add locale-guard utility functions
- Improved locale resolution logic in router by introducing utility functions for better readability and maintainability.
- Added `locale-guard.js` to encapsulate locale-related functions such as getting route locale, resolving locale for navigation, and applying locale navigation state.
- Updated JWT synchronization logic to streamline the handling of JWT from query parameters.
- Modified i18n messages test to check for coverage of registered locale message keys instead of extracting English source messages.
* 删除顶部栏语言和GitHub顺序设计文档
* fix: 修复前端设置初始化时未返回 domains 数组导致的 undefined 错误
* refactor(i18n): consolidate locale infrastructure
* fix(i18n): stabilize locale route switching
* fix(i18n): persist default locale selection
* fix(i18n): 修复前端设置初始化时未返回 domains 数组导致的 undefined 错误,统一按空数组兜底处理
feat(i18n): 添加 locale 别名处理,支持默认语言的重定向
test(i18n): 增加对默认语言别名重定向的测试用例
* refactor: replace useAppI18n with useScopedI18n in multiple components for improved localization management
* fix(tests): 移除不必要的 URL 断言以简化 Passkey 测试
* fix(i18n): 更新语言切换逻辑,确保使用当前语言设置进行路由导航
* fix(i18n): 强制路由切换以确保语言切换后正确导航
* refactor(i18n): 优化消息注册和路由本地化逻辑,移除冗余代码
* refactor(i18n): 拆分 API 文件以优化路由管理,更新语言处理逻辑
* fix: align i18n release notes and frontend test script
---
CHANGELOG.md | 1 +
CHANGELOG_EN.md | 1 +
e2e/tests/browser/locale-switch.spec.ts | 64 +
frontend/package.json | 2 +-
frontend/src/App.vue | 19 +-
frontend/src/components/AddressSelect.vue | 21 +-
frontend/src/components/AiExtractInfo.vue | 27 +-
frontend/src/components/MailBox.vue | 57 +-
.../src/components/MailContentRenderer.vue | 33 +-
frontend/src/components/SendBox.vue | 33 +-
frontend/src/components/Turnstile.vue | 46 +-
frontend/src/components/WebhookComponent.vue | 27 +-
frontend/src/i18n.ts | 15 -
frontend/src/i18n/app.ts | 12 +
frontend/src/i18n/index.ts | 18 +
frontend/src/i18n/locale-registry.ts | 107 +
frontend/src/i18n/locales/source/de.ts | 590 ++++
frontend/src/i18n/locales/source/es.ts | 590 ++++
frontend/src/i18n/locales/source/ja.ts | 590 ++++
frontend/src/i18n/locales/source/ptBR.ts | 590 ++++
frontend/src/i18n/message-registry.ts | 2465 +++++++++++++++++
frontend/src/i18n/messages.ts | 81 +
frontend/src/i18n/naive-locale.ts | 13 +
frontend/src/i18n/utils.ts | 138 +
frontend/src/router/index.js | 48 +-
frontend/src/store/index.js | 2 +
.../src/utils/__tests__/mail-actions.test.js | 265 --
frontend/src/utils/index.ts | 15 +-
frontend/src/views/Admin.vue | 87 +-
frontend/src/views/Footer.vue | 13 +-
frontend/src/views/Header.vue | 209 +-
frontend/src/views/Index.vue | 35 +-
frontend/src/views/User.vue | 19 +-
frontend/src/views/admin/Account.vue | 83 +-
frontend/src/views/admin/AccountSettings.vue | 107 +-
.../src/views/admin/AiExtractSettings.vue | 29 +-
frontend/src/views/admin/CreateAccount.vue | 33 +-
frontend/src/views/admin/DatabaseManager.vue | 27 +-
.../src/views/admin/IpBlacklistSettings.vue | 65 +-
frontend/src/views/admin/Mails.vue | 15 +-
frontend/src/views/admin/Maintenance.vue | 57 +-
.../src/views/admin/RoleAddressConfig.vue | 25 +-
frontend/src/views/admin/SendBox.vue | 15 +-
frontend/src/views/admin/SendMail.vue | 46 +-
frontend/src/views/admin/SenderAccess.vue | 41 +-
frontend/src/views/admin/Statistics.vue | 23 +-
frontend/src/views/admin/Telegram.vue | 35 +-
.../src/views/admin/UserAddressManagement.vue | 19 +-
frontend/src/views/admin/UserManagement.vue | 53 +-
.../src/views/admin/UserOauth2Settings.vue | 45 +-
frontend/src/views/admin/UserSettings.vue | 35 +-
frontend/src/views/admin/Webhook.vue | 23 +-
frontend/src/views/admin/WorkerConfig.vue | 1 -
frontend/src/views/common/AdminContact.vue | 13 +-
frontend/src/views/common/Appearance.vue | 35 +-
frontend/src/views/common/Login.vue | 55 +-
frontend/src/views/index/AccountSettings.vue | 43 +-
frontend/src/views/index/AddressBar.vue | 27 +-
frontend/src/views/index/Attachment.vue | 21 +-
frontend/src/views/index/AutoReply.vue | 30 +-
frontend/src/views/index/LocalAddress.vue | 27 +-
frontend/src/views/index/SendMail.vue | 50 +-
frontend/src/views/index/SimpleIndex.vue | 39 +-
frontend/src/views/index/TelegramAddress.vue | 25 +-
frontend/src/views/user/AddressManagement.vue | 37 +-
frontend/src/views/user/BindAddress.vue | 13 +-
frontend/src/views/user/UserBar.vue | 16 +-
frontend/src/views/user/UserLogin.vue | 47 +-
frontend/src/views/user/UserMailBox.vue | 15 +-
.../src/views/user/UserOauth2Callback.vue | 17 +-
frontend/src/views/user/UserSettings.vue | 41 +-
frontend/vite.config.js | 9 +-
72 files changed, 5577 insertions(+), 1993 deletions(-)
create mode 100644 e2e/tests/browser/locale-switch.spec.ts
delete mode 100644 frontend/src/i18n.ts
create mode 100644 frontend/src/i18n/app.ts
create mode 100644 frontend/src/i18n/index.ts
create mode 100644 frontend/src/i18n/locale-registry.ts
create mode 100644 frontend/src/i18n/locales/source/de.ts
create mode 100644 frontend/src/i18n/locales/source/es.ts
create mode 100644 frontend/src/i18n/locales/source/ja.ts
create mode 100644 frontend/src/i18n/locales/source/ptBR.ts
create mode 100644 frontend/src/i18n/message-registry.ts
create mode 100644 frontend/src/i18n/messages.ts
create mode 100644 frontend/src/i18n/naive-locale.ts
create mode 100644 frontend/src/i18n/utils.ts
delete mode 100644 frontend/src/utils/__tests__/mail-actions.test.js
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 @@