From 65a96670d52d5b318cb9d51e497ada4fcf846dd6 Mon Sep 17 00:00:00 2001 From: Arthur Koba Date: Sun, 25 Oct 2026 19:44:15 +0300 Subject: [PATCH] fix(i18n): use English runtime text and localize system UI --- .github/workflows/eslint.yml | 4 + .github/workflows/pr-template-check.yml | 2 +- Makefile | 2 +- docs/changelog/index.md | 7 + docs/design/log-language.md | 40 + docs/reference/configuration.md | 2 + frontend/app/(docs)/docs/api/page.tsx | 18 +- frontend/app/(docs)/docs/how-to-use/page.tsx | 14 +- .../app/(docs)/docs/privacy-policy/page.tsx | 15 +- .../app/(docs)/docs/terms-of-service/page.tsx | 17 +- .../components/access-log-utils.ts | 3 +- frontend/app/(main)/admin/demo/page.tsx | 36 +- .../(main)/admin/demo/tabs/controls-tab.tsx | 51 +- .../(main)/admin/demo/tabs/dashboard-tab.tsx | 20 +- .../app/(main)/admin/demo/tabs/table-tab.tsx | 36 +- .../admin/files/components/file-list.tsx | 3 +- .../admin/files/components/file-stats.tsx | 10 +- .../logs/components/access-analytics.tsx | 3 +- .../admin/logs/components/access-logs.tsx | 3 +- .../logs/components/log-storage-error.ts | 13 + .../admin/push/components/events-tab.tsx | 26 +- .../admin/push/components/settings-tab.tsx | 21 +- frontend/app/(main)/admin/system/page.tsx | 25 +- .../admin/tasks/components/task-manager.tsx | 2 - .../components/dashboard/node-rank-panel.tsx | 3 +- .../components/dashboard/world-stage-map.tsx | 7 +- .../(main)/components/placeholder-page.tsx | 13 +- .../nodes/components/node-observability.tsx | 5 +- .../app/(main)/nodes/components/node-utils.ts | 3 +- .../nodes/components/relay-node-detail.tsx | 6 +- .../(main)/proxy-routes/components/helpers.ts | 4 +- .../rate-limits/components/analysis-tab.tsx | 7 +- .../components/rate-pressure-chart.tsx | 3 +- frontend/app/(main)/waf/components/helpers.ts | 27 +- .../editor/components/editor-behavior.ts | 21 +- .../editor/components/geo-options.test.ts | 16 +- .../rules/editor/components/geo-options.ts | 86 +- .../editor/components/graph-validation.ts | 121 ++- .../editor/components/node-factory.test.ts | 29 +- .../rules/editor/components/node-factory.ts | 25 +- .../rules/editor/components/node-library.tsx | 6 +- .../editor/components/node-properties.tsx | 19 +- .../waf/rules/editor/components/rule-node.tsx | 4 +- .../waf/rules/editor/components/ua-options.ts | 8 +- frontend/app/(main)/waf/rules/editor/page.tsx | 6 +- .../[zoneId]/components/zone-overview.tsx | 10 +- .../websites/components/website-utils.ts | 13 +- frontend/app/not-found.tsx | 11 +- frontend/components/common/docs/api.tsx | 863 ++++++++-------- .../components/common/docs/how-to-use.tsx | 503 ++++----- .../common/docs/legal-page-layout.tsx | 22 +- frontend/components/common/docs/privacy.tsx | 305 +++--- frontend/components/common/docs/terms.tsx | 316 +++--- .../components/common/file-image-preview.tsx | 9 +- .../common/general/manage-pannel.tsx | 53 +- .../common/general/password-dialog.tsx | 15 +- frontend/components/common/home/home-main.tsx | 61 +- .../common/html-editor-workspace.tsx | 24 +- .../components/common/language-switcher.tsx | 16 +- .../common/settings/auth-source-modal.tsx | 8 +- .../common/settings/operation-tab.tsx | 10 +- .../components/common/settings/other-tab.tsx | 8 +- .../common/settings/security-tab.tsx | 10 +- .../components/common/settings/system-tab.tsx | 28 +- .../components/common/settings/templates.tsx | 58 +- .../components/common/user/file-manager.tsx | 3 +- frontend/components/data/rank-card.tsx | 6 +- frontend/components/data/rank-chart.tsx | 9 +- frontend/components/data/trend-chart.tsx | 5 +- .../components/home/developer-section.tsx | 38 +- frontend/components/home/footer-section.tsx | 36 +- frontend/components/home/hero-section.tsx | 23 +- .../layout/avater-style/NewYear.tsx | 5 +- frontend/components/layout/empty.tsx | 6 +- frontend/components/layout/error.tsx | 23 +- frontend/components/layout/loading.tsx | 9 +- .../components/providers/intl-provider.tsx | 15 +- .../components/providers/title-updater.tsx | 12 +- frontend/components/ui/calendar.tsx | 8 +- frontend/components/ui/tags-input.tsx | 8 +- frontend/contexts/admin-context.tsx | 15 +- frontend/contexts/admin-users-context.tsx | 21 +- frontend/contexts/user-context.tsx | 12 +- frontend/i18n/builtin-metadata.test.ts | 84 ++ frontend/i18n/builtin-metadata.ts | 45 + frontend/i18n/config.ts | 9 +- frontend/i18n/format.test.ts | 17 + frontend/i18n/format.ts | 14 +- frontend/i18n/runtime.test.ts | 127 +++ frontend/i18n/runtime.ts | 38 + frontend/lib/cap-solver.ts | 35 +- .../offline-page-templates/baohaus.ts | 173 +--- .../openflare/offline-page-templates/index.ts | 32 +- .../offline-page-templates/line-style.ts | 196 +--- .../offline-page-templates/minimalist.ts | 131 +-- .../origin-error-page-templates/baohaus.ts | 2 +- .../origin-error-page-templates/index.ts | 13 +- .../origin-error-page-templates/line-style.ts | 2 +- .../origin-error-page-templates/minimalist.ts | 2 +- frontend/lib/openflare/status-code-tags.ts | 35 +- frontend/lib/services/core/api-client.ts | 43 +- frontend/lib/services/core/errors.ts | 39 +- frontend/lib/task-param-utils.ts | 22 +- frontend/lib/utils.ts | 8 +- frontend/lib/utils/error-handling.ts | 12 +- frontend/lib/utils/metrics.ts | 19 +- frontend/lib/utils/search-data.ts | 131 +-- frontend/messages/fragments/admin.en.json | 124 +-- .../fragments/builtin-metadata.en.json | 113 ++ .../fragments/builtin-metadata.zh-CN.json | 113 ++ .../fragments/data-components.en.json | 7 + .../fragments/data-components.zh-CN.json | 7 + frontend/messages/fragments/languages.en.json | 3 + .../messages/fragments/languages.zh-CN.json | 3 + frontend/messages/fragments/runtime.en.json | 489 +++++++++ .../messages/fragments/runtime.zh-CN.json | 489 +++++++++ .../fragments/search-keywords.en.json | 36 + .../fragments/search-keywords.zh-CN.json | 36 + frontend/package.json | 2 +- frontend/proxy.ts | 13 +- frontend/scripts/check-runtime-language.mjs | 176 ++++ internal/apps/admin/db_manage/routers.go | 10 +- internal/apps/admin/errs.go | 10 +- internal/apps/admin/logs/routers.go | 16 +- internal/apps/admin/push/channels.go | 10 +- .../apps/admin/push/channels_definition.go | 60 +- .../admin/push/custom_events/admin_login.go | 8 +- .../push/custom_events/admin_login_test.go | 2 +- internal/apps/admin/push/i18n.go | 36 + internal/apps/admin/push/i18n_test.go | 23 + internal/apps/admin/push/logics.go | 11 +- internal/apps/admin/push/push_test.go | 2 +- internal/apps/admin/push/routers.go | 16 +- internal/apps/admin/push/task_listener.go | 2 +- internal/apps/admin/push/tasks.go | 26 +- internal/apps/admin/status/clickhouse.go | 12 +- internal/apps/admin/status/routers.go | 22 +- internal/apps/admin/system_config/errs.go | 10 +- internal/apps/admin/system_config/logics.go | 8 +- internal/apps/admin/system_config/routers.go | 37 +- internal/apps/admin/task/errs.go | 26 +- internal/apps/admin/task/i18n.go | 53 + internal/apps/admin/task/i18n_test.go | 45 + internal/apps/admin/task/routers.go | 28 +- internal/apps/admin/task/routers_test.go | 4 +- internal/apps/admin/template/errs.go | 14 +- internal/apps/admin/updater/errs.go | 18 +- internal/apps/admin/updater/logics.go | 32 +- internal/apps/admin/updater/restart_unix.go | 8 +- internal/apps/admin/user/errs.go | 28 +- internal/apps/admin/user/routers.go | 4 +- internal/apps/agent/agent/runner.go | 4 +- internal/apps/agent/config/config.go | 18 +- internal/apps/agent/nginx/manager.go | 30 +- internal/apps/agent/nginx/pow_assets.go | 34 +- .../pow_static/locales/challenge.en.json | 8 + .../pow_static/locales/challenge.zh-CN.json | 8 + internal/apps/cap/errs.go | 4 +- internal/apps/cap/routers.go | 2 +- internal/apps/flared/config/config.go | 8 +- internal/apps/oauth/errs.go | 26 +- internal/apps/openflare/agent/errs.go | 24 +- internal/apps/openflare/agent/routers.go | 4 +- internal/apps/openflare/agent/waf_ip_group.go | 4 +- .../apps/openflare/agent/waf_ip_group_test.go | 8 +- internal/apps/openflare/apiutil/bind.go | 4 +- internal/apps/openflare/apply_log/errs.go | 2 +- internal/apps/openflare/async_tasks.go | 44 +- internal/apps/openflare/async_tasks_test.go | 2 +- internal/apps/openflare/cloudflare/client.go | 10 +- internal/apps/openflare/cloudflare/errs.go | 42 +- internal/apps/openflare/cloudflare/routers.go | 2 +- internal/apps/openflare/cloudflare/tasks.go | 100 +- .../apps/openflare/config_version/errs.go | 10 +- .../apps/openflare/config_version/logics.go | 2 +- .../config_version/pages_snapshot.go | 26 +- .../apps/openflare/config_version/routers.go | 4 +- .../apps/openflare/config_version/snapshot.go | 14 +- .../config_version/waf_graph_snapshot_test.go | 4 +- internal/apps/openflare/flared/errs.go | 4 +- internal/apps/openflare/flared/logics.go | 6 +- internal/apps/openflare/geoip/lookup.go | 8 +- internal/apps/openflare/node/errs.go | 28 +- internal/apps/openflare/node/helpers.go | 28 +- internal/apps/openflare/node/logics.go | 8 +- internal/apps/openflare/node/routers.go | 4 +- .../observability/access_log_logics.go | 30 +- internal/apps/openflare/observability/errs.go | 2 +- internal/apps/openflare/option/errs.go | 12 +- .../openflare/option/openresty_validators.go | 58 +- .../option/openresty_validators_test.go | 14 +- internal/apps/openflare/option/routers.go | 2 +- internal/apps/openflare/option/validate.go | 30 +- internal/apps/openflare/origin/errs.go | 12 +- .../apps/openflare/pages/download_url_test.go | 2 +- internal/apps/openflare/pages/errs.go | 116 +-- .../openflare/pages/github_source_action.go | 20 +- internal/apps/openflare/pages/helpers.go | 30 +- internal/apps/openflare/pages/logics.go | 6 +- internal/apps/openflare/pages/logics_test.go | 4 +- .../apps/openflare/pages/package_metadata.go | 2 +- internal/apps/openflare/pages/rebind.go | 16 +- internal/apps/openflare/pages/routers.go | 4 +- .../apps/openflare/pages/source_provider.go | 6 +- .../apps/openflare/pages/source_scanner.go | 20 +- internal/apps/openflare/pages/source_sync.go | 10 +- internal/apps/openflare/pages/source_tasks.go | 20 +- internal/apps/openflare/proxy_route/errs.go | 16 +- .../apps/openflare/proxy_route/helpers.go | 2 +- internal/apps/openflare/relay/errs.go | 4 +- .../apps/openflare/tasks/log_db_switch.go | 58 +- .../openflare/tasks/log_db_switch_test.go | 6 +- internal/apps/openflare/tls/errs.go | 4 +- internal/apps/openflare/tls/logics.go | 4 +- internal/apps/openflare/tls/obtain.go | 18 +- internal/apps/openflare/tls/routers.go | 6 +- internal/apps/openflare/tls/tasks.go | 34 +- internal/apps/openflare/tls/tasks_test.go | 12 +- internal/apps/openflare/waf/graph_compile.go | 14 +- internal/apps/openflare/waf/graph_validate.go | 96 +- .../apps/openflare/waf/graph_validate_test.go | 50 +- internal/apps/openflare/waf/ip_group_sync.go | 28 +- internal/apps/openflare/waf/logics.go | 52 +- internal/apps/openflare/waf/rule_logics.go | 8 +- .../apps/openflare/waf/rule_logics_test.go | 4 +- internal/apps/openflare/waf/rule_routers.go | 6 +- internal/apps/openflare/zone/errs.go | 24 +- internal/apps/openflare/zone/legacy_import.go | 2 +- internal/apps/relay/config/config.go | 10 +- internal/apps/upload/filesrv/file_server.go | 8 +- .../apps/upload/handler/file_management.go | 12 +- internal/apps/upload/handler/routers.go | 10 +- internal/apps/upload/handler/stats.go | 3 +- internal/apps/upload/ingest/helpers.go | 8 +- internal/apps/upload/shared/errs.go | 56 +- internal/apps/upload/stats/category.go | 7 + internal/apps/upload/storage/storage_ops.go | 2 +- internal/apps/upload/task/cleanup.go | 42 +- internal/apps/upload/task/rebuild_stats.go | 14 +- .../apps/upload/task/storage_migration.go | 42 +- .../task/storage_migration_task_test.go | 2 +- internal/apps/upload/task/tasks.go | 22 +- internal/apps/upload/task/tasks_test.go | 8 +- internal/apps/user/access_tokens.go | 2 +- internal/apps/user/errs.go | 76 +- internal/apps/user/logics.go | 22 +- internal/apps/user/logics_test.go | 2 +- internal/apps/user/routers.go | 4 +- internal/apps/user/routers_test.go | 2 +- internal/apps/user/tasks.go | 32 +- internal/cmd/all.go | 14 +- internal/cmd/reset_passwd.go | 28 +- internal/cmd/reset_passwd_test.go | 6 +- internal/cmd/scheduler.go | 4 +- internal/cmd/worker.go | 4 +- internal/infra/objectstore/config.go | 2 +- internal/infra/task/errs.go | 29 +- internal/infra/task/executor.go | 44 +- internal/infra/task/executor_test.go | 10 +- internal/infra/task/permanent_error.go | 2 +- internal/infra/task/scheduler/scheduler.go | 4 +- internal/integration/githubrelease/client.go | 20 +- .../integration/githubrelease/transport.go | 6 +- internal/model/errs.go | 16 +- internal/model/task_execution.go | 2 +- internal/platform/bootstrap/bootstrap.go | 16 +- internal/platform/bootstrap/bootstrap_test.go | 10 +- internal/repository/auth_source.go | 2 +- internal/repository/errs.go | 20 +- internal/repository/logstore/cleanup.go | 8 +- internal/repository/user.go | 6 +- internal/shared/errs.go | 4 +- internal/shared/response/middleware.go | 12 +- internal/shared/response/middleware_test.go | 10 +- pkg/i18n/catalog.go | 286 ++++++ pkg/i18n/catalog_test.go | 105 ++ pkg/i18n/locales/en.json | 963 ++++++++++++++++++ pkg/i18n/locales/zh-CN.json | 963 ++++++++++++++++++ pkg/protocol/waf_ip_group_snapshot.go | 2 +- pkg/push/push.go | 2 +- pkg/render/openresty/render.go | 4 +- pkg/render/openresty/render_route.go | 4 +- pkg/render/openresty/service_worker.go | 8 +- pkg/render/openresty/status_codes.go | 12 +- pkg/util/format.go | 21 +- pkg/util/format_test.go | 12 +- scripts/audit-non-english.mjs | 159 +++ scripts/install-agent.sh | 77 +- scripts/uninstall-agent.sh | 27 +- 289 files changed, 8092 insertions(+), 3426 deletions(-) create mode 100644 docs/design/log-language.md create mode 100644 frontend/app/(main)/admin/logs/components/log-storage-error.ts create mode 100644 frontend/i18n/builtin-metadata.test.ts create mode 100644 frontend/i18n/builtin-metadata.ts create mode 100644 frontend/i18n/format.test.ts create mode 100644 frontend/i18n/runtime.test.ts create mode 100644 frontend/i18n/runtime.ts create mode 100644 frontend/messages/fragments/builtin-metadata.en.json create mode 100644 frontend/messages/fragments/builtin-metadata.zh-CN.json create mode 100644 frontend/messages/fragments/data-components.en.json create mode 100644 frontend/messages/fragments/data-components.zh-CN.json create mode 100644 frontend/messages/fragments/languages.en.json create mode 100644 frontend/messages/fragments/languages.zh-CN.json create mode 100644 frontend/messages/fragments/runtime.en.json create mode 100644 frontend/messages/fragments/runtime.zh-CN.json create mode 100644 frontend/messages/fragments/search-keywords.en.json create mode 100644 frontend/messages/fragments/search-keywords.zh-CN.json create mode 100644 frontend/scripts/check-runtime-language.mjs create mode 100644 internal/apps/admin/push/i18n.go create mode 100644 internal/apps/admin/push/i18n_test.go create mode 100644 internal/apps/admin/task/i18n.go create mode 100644 internal/apps/admin/task/i18n_test.go create mode 100644 internal/apps/agent/nginx/pow_static/locales/challenge.en.json create mode 100644 internal/apps/agent/nginx/pow_static/locales/challenge.zh-CN.json create mode 100644 pkg/i18n/catalog.go create mode 100644 pkg/i18n/catalog_test.go create mode 100644 pkg/i18n/locales/en.json create mode 100644 pkg/i18n/locales/zh-CN.json create mode 100644 scripts/audit-non-english.mjs diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 647c2eb2..5ed9df9a 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -33,3 +33,7 @@ jobs: - name: Run ESLint working-directory: frontend run: npx eslint . --max-warnings 0 + + - name: Check translation catalogs and runtime language + working-directory: frontend + run: node scripts/merge-i18n-fragments.mjs && pnpm check:i18n diff --git a/.github/workflows/pr-template-check.yml b/.github/workflows/pr-template-check.yml index e61365de..57510e61 100644 --- a/.github/workflows/pr-template-check.yml +++ b/.github/workflows/pr-template-check.yml @@ -25,7 +25,7 @@ jobs: // if any unchecked, fail the workflow if (unchecked.length > 0) { - core.setFailed(`PR checklist 未全部勾选,请确保所有 checklist 项都已勾选。未勾选项如下:\n${unchecked.join('\n')}`); + core.setFailed(`Complete all PR checklist items. Unchecked items:\n${unchecked.join('\n')}`); } else { console.log('all checklist items are checked.'); } diff --git a/Makefile b/Makefile index ff5e78e3..929fe3f6 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,7 @@ code-check: exit 1; \ fi golangci-lint run - cd frontend && node scripts/merge-i18n-fragments.mjs && pnpm tsc --noEmit --jsx preserve && npx eslint . --max-warnings 0 + cd frontend && node scripts/merge-i18n-fragments.mjs && pnpm check:i18n && pnpm tsc --noEmit --jsx preserve && npx eslint . --max-warnings 0 build-backend: @echo "==> Building backend version=$(VERSION) build_date=$(BUILD_DATE)..." diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 95a9ac29..18da8d3c 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -19,6 +19,13 @@ sidebar: false ### 🛠 修复 +- 将界面默认语言调整为英文,保留已有语言偏好和中文语言资源。 +- 静态页面以英文预渲染,并在水合界面绘制前应用已保存的语言偏好,避免英文界面刷新时先显示中文再切换。 +- 将 Server、Agent、任务执行详情、安装脚本及浏览器调试日志的固定描述统一为英文;任务和通知元数据、接口错误、页面标题、表单校验与内置页面模板按中英文语言包展示,历史任务记录在读取时兼容翻译,不回写用户数据或上传统计键。 +- 增加语言包一致性与运行时中文文案检查,防止页面和终端重新混入未登记的中文固定文案。 +- 语言选择器固定显示各语言的原生名称,并提供全仓库非 ASCII 文本清单命令,按模块和用途区分语言资源、源码、文档与兼容标识。 +- 根据实际界面审校英文通知、任务与模板提示,修正文档拼接缺少空格、重复用词和 Webhook JSON 示例转义;系统配置及内置模板的默认元数据按语言展示,保留自定义内容与原始编辑值,并改善窄屏 WAF 标题和通知模板说明的换行。 + - SQLite 文件数据库启动时自动启用 WAL 模式,改善读写并发;数据库导出改用一致性快照,确保包含 WAL 中已提交的数据。 - 修复 IP 组按规则自动抓取时会收录内网、回环及链路本地等地址的问题;规则测试同步过滤这些地址,历史抓取结果会在下次同步时清理。 diff --git a/docs/design/log-language.md b/docs/design/log-language.md new file mode 100644 index 00000000..48aa140a --- /dev/null +++ b/docs/design/log-language.md @@ -0,0 +1,40 @@ +# 运行时语言与国际化 + +## 运维输出与界面文案 + +Server、Agent、后台任务、安装/卸载脚本以及浏览器 PoW 调试日志的固定文案使用英文。语言切换不会改变控制台日志或 OTel 异常信息,便于跨语言检索和排障。任务参数、文件名、用户配置和第三方错误保留原文,可能包含中文。 + +前端继续使用 `next-intl`,以 `en` 为默认语言,保留 `zh-CN` 语言资源。当前阶段先整理英文基准文案并移除运行时中文硬编码,中文译文的进一步审校和统一验证安排在后续阶段。文案维护在 `frontend/messages/fragments`,通过 `node scripts/merge-i18n-fragments.mjs` 生成主消息包。技术示例与 HTML 使用原始消息读取,避免把代码中的花括号当作 ICU 参数。 + +语言选择器始终从各语言自己的资源读取名称:`English` 与 `中文`,不随当前界面语言翻译语言名称。 + +不在 React 渲染上下文中的浏览器代码(如网络异常和纯表单工具)通过 `frontend/i18n/runtime.ts` 调用同一套 `next-intl` 文案。消息在使用时解析语言,不在模块导入时固定语言。API 客户端发送当前界面语言的 `Accept-Language`。 + +## 服务端展示边界 + +`pkg/i18n/locales` 保留内置服务端文案的英文和中文版本,英文同时是运行时源码中的基准文案。展示接口依次采用 `NEXT_LOCALE` Cookie、`Accept-Language`,没有支持的语言时回退英文。 + +- 错误中间件先记录原始英文异常及 OTel 状态,再翻译 HTTP 响应中的 `error_msg`。登录流程使用的 `smtp_invalid:` 和 `need_email_code:` 控制前缀保持不变。 +- 任务类型、参数说明、执行记录和内置通知元数据使用请求内的副本进行翻译,避免修改共享注册表或持久化记录。 +- 默认定时任务名称可翻译;管理员自定义名称、通知接收目标和已保存的消息模板保留原文。 +- 新任务执行详情保存英文系统文案。历史中文文本通过已知的完整文案或格式模板在读取时展示为英文,不执行数据库回写。 + +格式模板只识别已登记的系统消息,并逐字保留捕获到的参数;时间戳也保留。未知消息原样返回。此兼容机制只用于系统拥有的展示字段,不能用来翻译任意用户数据,也不能通过比较翻译后的文本决定业务状态。任务重试和存储配置校验使用错误类型进行分类。 + +## 持久化兼容性 + +上传统计原有的六个中文分类名同时是数据库计数器键和 API 分类标识,不能当作标签直接改名。保留这些既有键,前端图表、图例和最大分类说明单独使用语言包标签;无需清空或重建现有统计。 + +历史 SQL 迁移、测试样例、中文项目文档以及用户已经保存的数据不属于待替换的运行时固定文案。离线页面和通知模板一旦由管理员选择、编辑并保存,就作为用户数据处理,不随界面语言切换而回写。 + +系统配置描述和内置邮件模板的名称、说明可能来自历史 seed。管理界面仅在配置键或内置模板键已登记、且文本与已知中英文默认值完全一致时,使用 `builtin-metadata` 语言分片展示;未知键、自定义描述与非内置模板保持原文。编辑表单保留原始状态,单纯保存其他字段不会把展示翻译写回数据库;邮件主题和正文始终按已保存内容显示。 + +## 校验与扩展 + +在仓库根目录运行 `node scripts/audit-non-english.mjs` 可生成完整的非 ASCII 文本清单,默认输出到被 Git 忽略的 `.codex-artifacts/non-english-audit/`。`occurrences.csv` 包含文件、行号、模块、分类和原文;JSON 另含字符位置、Unicode 码点、字符集及跳过的文件;`review-candidates.csv` 单列待审查源码。命令读取所有已跟踪和未被忽略的新文件,不按业务目录名称排除内容,也识别 Unicode 转义。 + +清单包含标点、表情、注释、文档、语言资源与历史迁移,不表示每个命中都需要替换。依赖、构建输出和私有环境文件不进入清单;二进制文件单独列出,不执行截图 OCR 或解包。生成的 Swagger 和中文文档站配置保留中文,既有上传分类键保留兼容性。字符集也不等同于语言,例如 Han 字符可能属于中文或日文。 + +`pnpm check:i18n` 校验中英文键一致性,并检查 Go、TypeScript/TSX 和安装脚本中的中文运行时文本;注释、语言资源、测试样例和上述六个持久化键有明确边界。该检查同时接入 `make code-check` 和现有前端 CI。 + +新增语言时,补齐前端分片、语言配置与 Provider 消息映射,以及服务端同名语言目录项,并验证格式占位符一致。新增系统消息仍以英文作为源文案,在相应语言资源中补齐译文。新增数据协议应使用稳定标识与参数,不再引入以中文显示文案充当持久化键的设计。 diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 380a4873..b4eab735 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -128,6 +128,8 @@ SQLite 文件数据库在启动时自动启用 WAL 模式;内存数据库保 | `log.max_size` | - | 单个日志文件的最大空间(MB),超出自动切割轮转 | `100` | | `log.max_age` | - | 历史切割日志文件最大保留天数 | `30` | +Server、Agent、调度器、任务框架及存储维护等运行时日志的固定描述使用英文,便于跨语言排障和检索。界面语言切换不会改变终端日志和 OTel 异常;接口错误、任务执行详情与内置元数据在展示时按语言翻译,历史记录不回写。用户配置名称及第三方错误保留原文,可能包含中文。现有日志格式、级别、参数及输出渠道保持不变,详见 [运行时语言与国际化](../design/log-language.md)。 + ### 6. 异步任务 Worker 队列配置 (`worker:`) | 配置文件 YAML 路径 | 对应覆盖环境变量 | 作用说明 | 默认值 | | --- | --- | --- | --- | diff --git a/frontend/app/(docs)/docs/api/page.tsx b/frontend/app/(docs)/docs/api/page.tsx index 406d8052..fe0041f6 100644 --- a/frontend/app/(docs)/docs/api/page.tsx +++ b/frontend/app/(docs)/docs/api/page.tsx @@ -1,17 +1,23 @@ +'use client'; + +import { useTranslations } from 'next-intl'; import { LegalPageLayout } from '@/components/common/docs/legal-page-layout'; -import { apiSections, DOCS_LAST_UPDATED } from '@/components/common/docs/api'; +import { + createApiSections, + DOCS_LAST_UPDATED, +} from '@/components/common/docs/api'; export default function ApiDocPage() { + const rt = useTranslations('runtime'); + return ( - OpenFlare 提供简单、强大的 API - 接口,支持多种编程语言和开发环境。通过标准化的 RESTful - 接口,您可以轻松地与后端服务进行集成与二次开发。 + {rt('openflare_provides_a_simple_powerful_api_for_ba400468')}

} /> diff --git a/frontend/app/(docs)/docs/how-to-use/page.tsx b/frontend/app/(docs)/docs/how-to-use/page.tsx index f4006098..69daed45 100644 --- a/frontend/app/(docs)/docs/how-to-use/page.tsx +++ b/frontend/app/(docs)/docs/how-to-use/page.tsx @@ -1,17 +1,21 @@ +'use client'; + +import { useTranslations } from 'next-intl'; import { LegalPageLayout } from '@/components/common/docs/legal-page-layout'; -import { howToUseSections } from '@/components/common/docs/how-to-use'; +import { createHowToUseSections } from '@/components/common/docs/how-to-use'; import { DOCS_LAST_UPDATED } from '@/components/common/docs/api'; export default function HowToUsePage() { + const rt = useTranslations('runtime'); + return ( - 欢迎使用 OpenFlare - 管理平台。本文档将引导您快速了解平台的核心组件、角色权限设计及二次开发流程,帮助您快速上手开发。 + {rt('welcome_to_the_openflare_management_platform_this_e8ecbe4d')}

} /> diff --git a/frontend/app/(docs)/docs/privacy-policy/page.tsx b/frontend/app/(docs)/docs/privacy-policy/page.tsx index 195ed2b8..35f1a627 100644 --- a/frontend/app/(docs)/docs/privacy-policy/page.tsx +++ b/frontend/app/(docs)/docs/privacy-policy/page.tsx @@ -1,18 +1,23 @@ +'use client'; + +import { useTranslations } from 'next-intl'; import { LegalPageLayout } from '@/components/common/docs/legal-page-layout'; -import { privacySections } from '@/components/common/docs/privacy'; +import { createPrivacySections } from '@/components/common/docs/privacy'; import { TERMS_LAST_UPDATED } from '@/components/common/docs/terms'; export default function PrivacyPolicyPage() { + const rt = useTranslations('runtime'); + return ( - 我们非常重视您的隐私保护。本政策将向您透明地展示我们如何收集、使用、存储您的个人信息, + {rt('we_value_your_privacy_this_policy_explains_2c9e3c0b')}
- 以及您享有的相关权利。 + {rt('and_the_rights_you_have_over_it_4560abf2')} } /> diff --git a/frontend/app/(docs)/docs/terms-of-service/page.tsx b/frontend/app/(docs)/docs/terms-of-service/page.tsx index ecb24e91..35d77f36 100644 --- a/frontend/app/(docs)/docs/terms-of-service/page.tsx +++ b/frontend/app/(docs)/docs/terms-of-service/page.tsx @@ -1,16 +1,25 @@ +'use client'; + +import { useTranslations } from 'next-intl'; import { LegalPageLayout } from '@/components/common/docs/legal-page-layout'; import { TERMS_LAST_UPDATED, - termsSections, + createTermsSections, } from '@/components/common/docs/terms'; export default function TermsOfServicePage() { + const rt = useTranslations('runtime'); + return ( 为了保障您的合法权益,请您仔细阅读以下条款。} + sections={createTermsSections(rt)} + description={ + + {rt('please_read_the_following_terms_carefully_to_c46a6a61')} + + } /> ); } diff --git a/frontend/app/(main)/access-logs/components/access-log-utils.ts b/frontend/app/(main)/access-logs/components/access-log-utils.ts index 8f16333a..3dca4faf 100644 --- a/frontend/app/(main)/access-logs/components/access-log-utils.ts +++ b/frontend/app/(main)/access-logs/components/access-log-utils.ts @@ -1,3 +1,4 @@ +import { getFormattingLocale } from '@/i18n/format'; export type AccessLogTab = 'overview' | 'ips' | 'list'; export type SearchDraft = { @@ -66,7 +67,7 @@ export function parseSortValue(value: string) { } export function formatCompactNumber(value: number) { - return new Intl.NumberFormat('zh-CN', { + return new Intl.NumberFormat(getFormattingLocale(), { notation: value >= 10000 ? 'compact' : 'standard', maximumFractionDigits: 1, }).format(value); diff --git a/frontend/app/(main)/admin/demo/page.tsx b/frontend/app/(main)/admin/demo/page.tsx index aaafdd69..7eba8158 100644 --- a/frontend/app/(main)/admin/demo/page.tsx +++ b/frontend/app/(main)/admin/demo/page.tsx @@ -1,8 +1,8 @@ +'use client'; + // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -'use client'; - import * as React from 'react'; import { useTranslations } from 'next-intl'; import { Code } from 'lucide-react'; @@ -22,6 +22,8 @@ import { TableTab } from './tabs/table-tab'; import { ControlsTab } from './tabs/controls-tab'; export default function HeaderDemoPage() { + const rt = useTranslations('runtime'); + const t = useTranslations('admin.demo'); const [activeTab, setActiveTab] = React.useState('dashboard'); @@ -59,7 +61,8 @@ export default function HeaderDemoPage() { {t('rules.structureFocus.description')}
  • - {t('rules.containerAlignment.title')}:使用{' '} + {t('rules.containerAlignment.title')} + {rt('use_1b3d699e')}{' '} flex items-center gap-2 {' '} @@ -67,22 +70,23 @@ export default function HeaderDemoPage() { py-6 px-1 {' '} - (或{' '} + {rt('or_07d4e3c0')}{' '} py-6 ) {t('rules.containerAlignment.description2')}
  • - {t('rules.iconPresentation.title')}:直接将 - Lucide 图标组件嵌套于标题容器中,应用{' '} + {t('rules.iconPresentation.title')} + {rt('place_the_lucide_icon_directly_inside_the_104f794c')}{' '} size-5 text-primary {' '} {t('rules.iconPresentation.description')}
  • - {t('rules.textStyle.title')}:文本使用且仅使用{' '} + {t('rules.textStyle.title')} + {rt('use_only_5e12eecb')}{' '} {'h1 className="text-2xl font-semibold tracking-tight"'} @@ -93,10 +97,8 @@ export default function HeaderDemoPage() { {t('rules.textStyle.description2')}
  • - {t('rules.tabsModular.title')}:凡是带有多个 - Tab 页切换的页面,**禁止**将所有 Tab - 的渲染代码堆积在同一个物理页面主文件内。每个 Tab 页对应的 - Content 必须单独拆分为独立组件文件(如{' '} + {t('rules.tabsModular.title')} + {rt('for_pages_with_multiple_tabs_keep_each_5f721959')}{' '} tabs/events-tab.tsx {' '} @@ -107,8 +109,8 @@ export default function HeaderDemoPage() { {t('rules.tabsModular.description2')}
  • - {t('rules.complexityDriven.title')}:不仅是 - Tabs 切换,当一个页面代码行数超过 600 + {t('rules.complexityDriven.title')} + {rt('beyond_tabs_when_a_page_exceeds_600_d152cfdc')} {t('rules.complexityDriven.description')}{' '} app/(main)/admin/database/components/ @@ -121,19 +123,19 @@ export default function HeaderDemoPage() { /admin/database - ) 的{' '} + {rt('message_18ab4763')}{' '} table-browser.tsx - 、 + {rt('list_separator')} cache-manager.tsx {' '} - 与{' '} + {rt('and_de1d5dd0')}{' '} sql-console.tsx - 。 + {rt('sentence_end')}
  • diff --git a/frontend/app/(main)/admin/demo/tabs/controls-tab.tsx b/frontend/app/(main)/admin/demo/tabs/controls-tab.tsx index 52f0d775..000586da 100644 --- a/frontend/app/(main)/admin/demo/tabs/controls-tab.tsx +++ b/frontend/app/(main)/admin/demo/tabs/controls-tab.tsx @@ -1,8 +1,8 @@ +'use client'; + // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -'use client'; - import * as React from 'react'; import { useTranslations } from 'next-intl'; import { Code } from 'lucide-react'; @@ -26,6 +26,8 @@ import { Checkbox } from '@/components/ui/checkbox'; import { Button } from '@/components/ui/button'; export function ControlsTab() { + const rt = useTranslations('runtime'); + const t = useTranslations('admin.demo.controls'); const [formData, setFormData] = React.useState({ name: '', @@ -166,78 +168,71 @@ export function ControlsTab() { {t('spec.unifiedHeight')}

    - 输入框 ( + {rt('inputs_fabee312')} Input - ) 与下拉选择器 ( + {rt('and_selectors_e4875cdf')} SelectTrigger - ) 的高度应统一限制为小号高度{' '} + {rt('should_share_the_compact_height_f1fe2218')}{' '} h-8 - ,字体大小使用{' '} + {rt('with_font_size_e7c29991')}{' '} text-xs - ,并使用{' '} + {rt('and_use_ca398a12')}{' '} shadow-none {' '} - 去除原生的重叠阴影。 + {rt('to_remove_overlapping_native_shadows_dd65b475')}

    {t('spec.checkboxAlignment')}

    - Checkbox 应在左侧垂直偏上对齐,使用容器{' '} + {rt('align_checkboxes_toward_the_top_left_using_092dcef0')}{' '} flex items-start gap-2.5 {' '} - 排列。Checkbox 组件本身添加{' '} + {rt('add_bf15da3a')}{' '} mt-0.5 - ,从而在多行描述文本的情况下依然保持良好的顶对齐,避免出现居中对齐导致的凌乱感。 + {rt('to_the_checkbox_so_multiline_descriptions_stay_1bb85c4b')}

    {t('spec.buttonStrategy')}

    - 普通主表单及新增/保存按钮,一律使用默认 Primary 按钮或次要{' '} + {rt('use_the_default_primary_or_secondary_db7513e5')}{' '} {'variant="secondary"'} {' '} - 按钮,小号高度统一采用{' '} + {rt('button_for_forms_and_create_save_actions_a657dba8')}{' '} h-8 text-xs - 。线框按钮必须带有{' '} + {rt('outline_buttons_must_include_the_66ace005')}{' '} border-dashed shadow-none {' '} - 类。 + {rt('class_43eff5ff')}

    -
    {`// 1. Label 与 Input 对齐`}
    -
    {`
    - - -
    `}
    -
    {`// 2. Checkbox 顶对齐`}
    -
    {`
    - -
    - -

    多行说明内容

    -
    -
    `}
    +
    {rt('1_align_label_and_input_32d4558d')}
    +
    {rt.raw('div_classname_space_y_1_5_label_32230ad7')}
    +
    + {rt('2_align_checkbox_to_the_top_bdf68ad8')} +
    +
    {rt.raw('div_classname_flex_items_start_gap_2_ccf0d526')}
    diff --git a/frontend/app/(main)/admin/demo/tabs/dashboard-tab.tsx b/frontend/app/(main)/admin/demo/tabs/dashboard-tab.tsx index 9837d581..819866e7 100644 --- a/frontend/app/(main)/admin/demo/tabs/dashboard-tab.tsx +++ b/frontend/app/(main)/admin/demo/tabs/dashboard-tab.tsx @@ -1,8 +1,8 @@ +'use client'; + // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -'use client'; - import * as React from 'react'; import { useTranslations } from 'next-intl'; import { Code, Cpu, Database, HardDrive } from 'lucide-react'; @@ -10,6 +10,8 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; export function DashboardTab() { + const rt = useTranslations('runtime'); + const t = useTranslations('admin.demo.dashboard'); return (
    @@ -129,17 +131,9 @@ export function DashboardTab() {
    -            {`
    -  
    -    指标标题
    -    
    -  
    -  
    -    
    42.8 %
    - -

    状态说明小字

    -
    -
    `} + {rt.raw( + 'card_classname_border_dashed_shadow_none_cardheader_35727acf', + )}
    diff --git a/frontend/app/(main)/admin/demo/tabs/table-tab.tsx b/frontend/app/(main)/admin/demo/tabs/table-tab.tsx index 29f71b56..72a48f0a 100644 --- a/frontend/app/(main)/admin/demo/tabs/table-tab.tsx +++ b/frontend/app/(main)/admin/demo/tabs/table-tab.tsx @@ -1,8 +1,8 @@ +'use client'; + // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -'use client'; - import * as React from 'react'; import { useTranslations } from 'next-intl'; import { Code, Edit, ListFilter, Plus, Search, Trash2 } from 'lucide-react'; @@ -20,6 +20,8 @@ import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; export function TableTab() { + const rt = useTranslations('runtime'); + const t = useTranslations('admin.demo.table'); const mockTableData = [ { @@ -193,7 +195,7 @@ export function TableTab() { border border-dashed shadow-none rounded-lg overflow-hidden bg-background
    - 。 + {rt('sentence_end')}
  • {t('spec.headerBg')} @@ -204,7 +206,7 @@ export function TableTab() { text-xs font-semibold - 。 + {rt('sentence_end')}
  • {t('spec.cellDivider')} @@ -215,14 +217,14 @@ export function TableTab() { hover:bg-muted/10 transition-colors - 。 + {rt('sentence_end')}
  • {t('spec.monoFont')} font-mono text-xs - 。 + {rt('sentence_end')}
  • {t('spec.badgeStyle')} @@ -233,27 +235,7 @@ export function TableTab() {
  • -            {`
    - - - - 编号 - 状态 - - - - - ID-1001 - - - - 成功 - - - - -
    -
    `} + {rt.raw('div_classname_border_border_dashed_shadow_none_b430b520')}
    diff --git a/frontend/app/(main)/admin/files/components/file-list.tsx b/frontend/app/(main)/admin/files/components/file-list.tsx index cf00aff2..b2188627 100644 --- a/frontend/app/(main)/admin/files/components/file-list.tsx +++ b/frontend/app/(main)/admin/files/components/file-list.tsx @@ -1,5 +1,6 @@ 'use client'; +import { getFormattingLocale } from '@/i18n/format'; import * as React from 'react'; import { useTranslations } from 'next-intl'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; @@ -72,7 +73,7 @@ function getFileIcon(mimeType: string, className = 'size-8') { } function formatDate(dateStr: string) { - return new Date(dateStr).toLocaleString('zh-CN', { + return new Date(dateStr).toLocaleString(getFormattingLocale(), { year: 'numeric', month: '2-digit', day: '2-digit', diff --git a/frontend/app/(main)/admin/files/components/file-stats.tsx b/frontend/app/(main)/admin/files/components/file-stats.tsx index a03dc0b1..219461ee 100644 --- a/frontend/app/(main)/admin/files/components/file-stats.tsx +++ b/frontend/app/(main)/admin/files/components/file-stats.tsx @@ -50,6 +50,12 @@ const categoryMap: Record = { 其他: 'others', }; +// The API retains persisted category keys; labels always follow the UI locale. +function categoryTranslationKey(name: string) { + const key = categoryMap[name] || 'others'; + return `stats.cat${key.charAt(0).toUpperCase()}${key.slice(1)}`; +} + function useStatsChartConfig() { const t = useTranslations('admin.files'); return { @@ -158,11 +164,11 @@ export function FileStats() { stats.categories.forEach((cat) => { if (cat.count > maxCount) { maxCount = cat.count; - maxCountName = cat.name; + maxCountName = t(categoryTranslationKey(cat.name)); } if (cat.size > maxSize) { maxSize = cat.size; - maxSizeName = cat.name; + maxSizeName = t(categoryTranslationKey(cat.name)); } }); diff --git a/frontend/app/(main)/admin/logs/components/access-analytics.tsx b/frontend/app/(main)/admin/logs/components/access-analytics.tsx index 43343fa2..280a28e0 100644 --- a/frontend/app/(main)/admin/logs/components/access-analytics.tsx +++ b/frontend/app/(main)/admin/logs/components/access-analytics.tsx @@ -17,6 +17,7 @@ limitations under the License. 'use client'; import { useCallback, useEffect, useState } from 'react'; +import { isLogStorageDisabledMessage } from './log-storage-error'; import { BarChart3, Globe, @@ -117,7 +118,7 @@ export function AccessAnalytics() { const errorInstance = err instanceof Error ? err : new Error(t('fetchFailed')); const errMsg = errorInstance.message || ''; - if (errMsg.includes('ClickHouse') || errMsg.includes('未启用')) { + if (isLogStorageDisabledMessage(errMsg)) { setClickhouseDisabled(true); } else { setError(errorInstance); diff --git a/frontend/app/(main)/admin/logs/components/access-logs.tsx b/frontend/app/(main)/admin/logs/components/access-logs.tsx index b128132a..0e1340ea 100644 --- a/frontend/app/(main)/admin/logs/components/access-logs.tsx +++ b/frontend/app/(main)/admin/logs/components/access-logs.tsx @@ -17,6 +17,7 @@ limitations under the License. 'use client'; import { useMemo, useState } from 'react'; +import { isLogStorageDisabledMessage } from './log-storage-error'; import { useQuery } from '@tanstack/react-query'; import { toast } from 'sonner'; import { format } from 'date-fns'; @@ -94,7 +95,7 @@ function statusVariant(status: number) { function isClickhouseDisabledError(error: Error) { const errMsg = error.message || ''; - return errMsg.includes('ClickHouse') || errMsg.includes('未启用'); + return isLogStorageDisabledMessage(errMsg); } export function AccessLogs() { diff --git a/frontend/app/(main)/admin/logs/components/log-storage-error.ts b/frontend/app/(main)/admin/logs/components/log-storage-error.ts new file mode 100644 index 00000000..30df331f --- /dev/null +++ b/frontend/app/(main)/admin/logs/components/log-storage-error.ts @@ -0,0 +1,13 @@ +import { getRuntimeTranslator } from '@/i18n/runtime'; + +export function isLogStorageDisabledMessage(message: string): boolean { + const normalized = message.toLowerCase(); + return ( + message.includes('ClickHouse') || + (['en', 'zh-CN'] as const).some((locale) => + normalized.includes( + getRuntimeTranslator(locale)('disabled_f95ea7f4').toLowerCase(), + ), + ) + ); +} diff --git a/frontend/app/(main)/admin/push/components/events-tab.tsx b/frontend/app/(main)/admin/push/components/events-tab.tsx index d0acce60..fc42fad1 100644 --- a/frontend/app/(main)/admin/push/components/events-tab.tsx +++ b/frontend/app/(main)/admin/push/components/events-tab.tsx @@ -1,8 +1,8 @@ +'use client'; + // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -'use client'; - import * as React from 'react'; import { useTranslations } from 'next-intl'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; @@ -81,6 +81,8 @@ import type { import { PushService } from '@/lib/services/push'; export function EventsTab() { + const rt = useTranslations('runtime'); + const t = useTranslations('admin.push.events'); const queryClient = useQueryClient(); const [deleteTarget, setDeleteTarget] = React.useState( @@ -706,11 +708,11 @@ export function EventsTab() {
    -
    +
    - + {newEventType === 'task' ? t('taskTemplateVars') : t('eventTemplateVars')} @@ -721,7 +723,11 @@ export function EventsTab() { onChange={(e) => setNewEventTemplate(e.target.value)} rows={6} className='text-xs font-mono' - placeholder='{"title": "管理员登录提醒", "content": "管理员 {{user.username}} ...", "level": "INFO"}' + placeholder={rt.raw( + newEventType === 'task' + ? 'task_notification_template_example' + : 'title_administrator_login_alert_content_administrator_user_6f32cf7c', + )} />
    @@ -886,11 +892,11 @@ export function EventsTab() {
    -
    +
    - + {selectedEvent.task_type ? t('taskTemplateVars') : t('eventTemplateVars')} @@ -901,7 +907,11 @@ export function EventsTab() { onChange={(e) => setEventTemplate(e.target.value)} rows={6} className='text-xs font-mono' - placeholder='{"title": "管理员登录提醒", "content": "管理员 {{user.username}} ...", "level": "INFO"}' + placeholder={rt.raw( + selectedEvent.task_type + ? 'task_notification_template_example' + : 'title_administrator_login_alert_content_administrator_user_6f32cf7c', + )} />
    diff --git a/frontend/app/(main)/admin/push/components/settings-tab.tsx b/frontend/app/(main)/admin/push/components/settings-tab.tsx index 30153700..11e23f13 100644 --- a/frontend/app/(main)/admin/push/components/settings-tab.tsx +++ b/frontend/app/(main)/admin/push/components/settings-tab.tsx @@ -1,8 +1,8 @@ +'use client'; + // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -'use client'; - import * as React from 'react'; import { useTranslations } from 'next-intl'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; @@ -62,6 +62,8 @@ import type { import { PushService } from '@/lib/services/push'; export function SettingsTab() { + const rt = useTranslations('runtime'); + const t = useTranslations('admin.push.settings'); const queryClient = useQueryClient(); const [deleteTarget, setDeleteTarget] = React.useState( @@ -606,7 +608,7 @@ export function SettingsTab() { toast.success(t('loadedFeishuTemplate')); }} > - 飞书 Webhook + {rt('lark_webhook_c2e0b312')} ))}
    diff --git a/frontend/app/(main)/waf/rules/editor/components/node-properties.tsx b/frontend/app/(main)/waf/rules/editor/components/node-properties.tsx index 5a498a93..a5319625 100644 --- a/frontend/app/(main)/waf/rules/editor/components/node-properties.tsx +++ b/frontend/app/(main)/waf/rules/editor/components/node-properties.tsx @@ -1,6 +1,6 @@ import { CircleHelp, Settings2 } from 'lucide-react'; -import { useTranslations } from 'next-intl'; -import { useState } from 'react'; +import { useLocale, useTranslations } from 'next-intl'; +import { useMemo, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; @@ -35,8 +35,8 @@ import { } from '@/components/ui/tooltip'; import type { WAFIPGroup, WAFRuleNode } from '@/lib/services/openflare'; -import { countryOptions, regionOptions, type GeoOption } from './geo-options'; -import { NODE_TYPE_LABELS } from './node-factory'; +import { createGeoOptions, type GeoOption } from './geo-options'; +import { NODE_TYPE_LABEL_KEYS } from './node-factory'; import { UA_BROWSER_OPTIONS, UA_OS_OPTIONS } from './ua-options'; export function NodeProperties({ @@ -78,6 +78,14 @@ function PropertyFields({ onChange: (node: WAFRuleNode) => void; }) { const t = useTranslations('waf.editor'); + const locale = useLocale(); + const { countryOptions, regionOptions } = useMemo( + () => + node.type === 'geo_match' + ? createGeoOptions(locale) + : { countryOptions: [], regionOptions: [] }, + [locale, node.type], + ); if (node.type === 'start' || node.type === 'allow') return (

    {t('systemNodeNoConfig')}

    @@ -518,13 +526,14 @@ function DisplayNameField({ onChange: (node: WAFRuleNode) => void; }) { const t = useTranslations('waf.editor'); + const rt = useTranslations('runtime'); return ( {t('displayName')} onChange({ ...node, label: event.target.value })} /> diff --git a/frontend/app/(main)/waf/rules/editor/components/rule-node.tsx b/frontend/app/(main)/waf/rules/editor/components/rule-node.tsx index e7b72782..56d3ee50 100644 --- a/frontend/app/(main)/waf/rules/editor/components/rule-node.tsx +++ b/frontend/app/(main)/waf/rules/editor/components/rule-node.tsx @@ -1,4 +1,5 @@ import { Handle, Position, type NodeProps } from '@xyflow/react'; +import { useTranslations } from 'next-intl'; import { Ban, Fingerprint, @@ -42,10 +43,11 @@ const outputHandles: Partial> = { }; export function RuleNode({ data, selected }: NodeProps) { + const rt = useTranslations('runtime'); const value = data as RuleFlowNodeData; const { rule, issues } = value; const { icon: Icon } = meta[rule.type]; - const title = displayNodeTitle(rule); + const title = displayNodeTitle(rule, rt); return (
    {t('back')} -
    -
    +
    +

    {ruleQuery.data.name} @@ -235,7 +235,7 @@ function EditorContent() { {dirty && {t('unsaved')}}

    -
    +
    value - .toLocaleDateString('en-GB', { day: 'numeric', month: 'long' }) + .toLocaleDateString(getFormattingLocale(), { + day: 'numeric', + month: 'long', + }) .toUpperCase(); return `${fmt(start)} — ${fmt(end)}`; } diff --git a/frontend/app/(main)/websites/components/website-utils.ts b/frontend/app/(main)/websites/components/website-utils.ts index 8f42b1e4..79c12564 100644 --- a/frontend/app/(main)/websites/components/website-utils.ts +++ b/frontend/app/(main)/websites/components/website-utils.ts @@ -1,3 +1,4 @@ +import { runtimeText } from '@/i18n/runtime'; import type { TlsCertificateFileImportPayload, TlsCertificateItem, @@ -17,7 +18,8 @@ export type TranslateFn = ( export function getErrorMessage(error: unknown, fallback?: string) { return error instanceof Error ? error.message - : (fallback ?? '请求失败,请稍后重试。'); + : (fallback ?? + runtimeText('request_failed_please_try_again_later_8977b79a')); } export function getMatchTypeMeta(domain: string): { @@ -25,8 +27,8 @@ export function getMatchTypeMeta(domain: string): { tone: StatusTone; } { return domain.startsWith('*.') - ? { label: '通配符', tone: 'warning' } - : { label: '精确匹配', tone: 'info' }; + ? { label: runtimeText('wildcard_c6b0739e'), tone: 'warning' } + : { label: runtimeText('exact_match_391fe725'), tone: 'info' }; } export function getCertificateStatus( @@ -57,7 +59,10 @@ export function getCertificateStatus( export function buildCertificateLabel(certificate: TlsCertificateItem) { return certificate.not_after - ? `${certificate.name}(到期:${formatDateTime(certificate.not_after)})` + ? runtimeText('value0_expires_value1_7d8600f8', { + value0: certificate.name, + value1: formatDateTime(certificate.not_after), + }) : certificate.name; } diff --git a/frontend/app/not-found.tsx b/frontend/app/not-found.tsx index 8dccb356..8f34f4e1 100644 --- a/frontend/app/not-found.tsx +++ b/frontend/app/not-found.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useTranslations } from 'next-intl'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { motion } from 'motion/react'; @@ -11,6 +12,8 @@ import { Button } from '@/components/ui/button'; * 当用户访问不存在的页面时显示 */ export default function NotFound() { + const rt = useTranslations('runtime'); + const router = useRouter(); return ( @@ -43,9 +46,9 @@ export default function NotFound() { className='space-y-8' >

    - 抱歉,您访问的页面似已迷失在数字星云中。 + {rt('sorry_the_page_you_requested_seems_to_ba66cd83')}
    - 请检查路径或返回安全地带。 + {rt('check_the_path_or_return_to_safety_b1f31888')}

    @@ -56,7 +59,7 @@ export default function NotFound() { className='rounded-full w-24 text-xs border-foreground/10 hover:bg-foreground/10 transition-all duration-300' > - 上一页 + {rt('previous_c9b9ae7a')} @@ -66,7 +69,7 @@ export default function NotFound() { className='rounded-full w-24 text-xs hover:bg-primary/80 transition-all duration-300' > - 首页 + {rt('home_203c08e0')}
    diff --git a/frontend/components/common/docs/api.tsx b/frontend/components/common/docs/api.tsx index 7b38e8c6..817799c3 100644 --- a/frontend/components/common/docs/api.tsx +++ b/frontend/components/common/docs/api.tsx @@ -1,3 +1,4 @@ +import type { RuntimeTranslator } from '@/i18n/runtime'; import { type PolicySection } from './types'; import { CodeBlock } from '@/components/ui/code-block'; import { @@ -16,57 +17,65 @@ export const DOCS_LAST_UPDATED = '2026-06-07'; * API 文档 * ------------------------------------------------------------------ */ -export const apiSections: PolicySection[] = [ - { - value: 'api-specs', - title: '1. 接口规范与鉴权说明', - content: ( -
    -
    -

    - 平台统一接口调用格式规范以及开发者访问令牌鉴权方式说明 -

    -
    +export function createApiSections(rt: RuntimeTranslator): PolicySection[] { + return [ + { + value: 'api-specs', + title: rt('1_api_conventions_and_authentication_e6d4df61'), + content: ( +
    +
    +

    + {rt( + 'standard_api_request_conventions_and_developer_access_39eb956e', + )} +

    +
    -

    - 1.1 统一响应格式 -

    -

    系统所有 API 接口均遵循标准 JSON 响应结构:

    - - - - 字段 - 类型 - 说明 - - - - - - error_msg - - string - - 错误信息。请求成功时为空字符串 - `""`,失败时包含错误详情描述。 - - - - data - any - - 接口返回的具体数据内容。请求失败或无数据返回时为 `null`。 - - - - +

    + {rt('1_1_standard_response_format_5cddae2f')} +

    +

    {rt('all_api_endpoints_use_a_standard_json_87c92fb0')}

    + + + + + {rt('field_49ecd0e3')} + + + {rt('type_ba40014f')} + + {rt('description_4262c45d')} + + + + + + error_msg + + string + + {rt('error_message_empty_on_success_contains_error_d89d457f')} + + + + + data + + any + + {rt('the_response_payload_null_when_the_request_97b8b29c')} + + + + -

    成功响应示例:

    - {rt('successful_response_example_8917e853')}

    + + language='json' + /> -

    失败响应示例:

    - +

    {rt('failed_response_example_eae899e1')}

    + -

    - 1.2 鉴权方式 -

    -

    - 除了公共公开接口(如登录、注册、配置)外,受保护的接口需要携带凭证才能正常访问: -

    -
      -
    • - Session 凭证:浏览器环境下支持利用常规 Session - Cookie 会话保持登录。 -
    • -
    • - AccessToken 令牌: - 供后台调用或第三方应用集成使用。客户端生成 API - 访问令牌后,需要在请求头(Request Header)中携带以进行身份校验。 -
    • -
    -
    -

    - 支持携带令牌的请求头格式(二选一): +

    + {rt('1_2_authentication_methods_3c5db38a')} +

    +

    + {rt( + 'protected_endpoints_require_credentials_except_public_endpoints_1fa0a24c', + )}

    -
      +
      • - - Authorization: Bearer at_xxx - + {rt('session_credentials_de72024d')}{' '} + {rt('browser_clients_can_stay_signed_in_using_f316279b')}
      • - - X-Access-Token: at_xxx - + {rt('access_tokens_8de23550')}{' '} + {rt('for_backend_calls_and_third_party_integrations_dfd8bf0f')}
      +
      +

      + {rt('use_either_of_the_following_request_headers_1e03b04b')} +

      +
        +
      • + + Authorization: Bearer at_xxx + +
      • +
      • + + X-Access-Token: at_xxx + +
      • +
      +
    -
    - ), - children: [ - { value: '1-1-response-format', title: '1.1 统一响应格式' }, - { value: '1-2-authentication', title: '1.2 鉴权方式' }, - ], - }, - { - value: 'auth-apis', - title: '2. 用户与认证接口', - content: ( -
    -

    - 2.1 用户注册 -

    -

    - 接口:POST{' '} - - /api/v1/user/register - -

    -

    - 说明:注册本地账户(在后台注册开关开启状态下)。 -

    - - - - 参数 - 必填 - 类型 - 说明 - - - - - - username - - 是 - string - 用户名,必须唯一且无空格。 - - - - password - - 是 - string - 密码,长度必须大于等于 8 位。 - - - - nickname - - 否 - string - 昵称。未传时默认与用户名一致。 - - - + ), + children: [ + { + value: '1-1-response-format', + title: rt('1_1_standard_response_format_5cddae2f'), + }, + { + value: '1-2-authentication', + title: rt('1_2_authentication_methods_3c5db38a'), + }, + ], + }, + { + value: 'auth-apis', + title: rt('2_user_and_authentication_endpoints_4a89d429'), + content: ( +
    +

    + {rt('2_1_user_registration_4f332f85')} +

    +

    + {rt('endpoint_79a2369a')} POST{' '} + + /api/v1/user/register + +

    +

    + {rt('description_3e633a20')}{' '} + {rt('register_a_local_account_when_registration_is_938891dc')} +

    + + + + + {rt('parameter_9634fb08')} + + + {rt('required_11da9dc4')} + + {rt('type_ba40014f')} + {rt('description_4262c45d')} + + + + + + username + + {rt('yes_b5141d3d')} + string + + {rt('username_must_be_unique_and_contain_no_e81c1e5c')} + + + + + password + + {rt('yes_b5141d3d')} + string + + {rt('password_at_least_8_characters_37cdc807')} + + + + + nickname + + {rt('no_0c70665b')} + string + + {rt( + 'nickname_defaults_to_the_username_when_omitted_676aae7e', + )} + + + + -

    - 2.2 密码登录 -

    -

    - 接口:POST{' '} - - /api/v1/user/login - -

    -

    - 说明:通过常规用户名密码进行登录校验,成功后建立 - Session Cookie 会话。 -

    - - - - 参数 - 必填 - 类型 - 说明 - - - - - - username - - 是 - string - 用户名 - - - - password - - 是 - string - 密码 - - - +

    + {rt('2_2_password_login_271483ba')} +

    +

    + {rt('endpoint_79a2369a')} POST{' '} + + /api/v1/user/login + +

    +

    + {rt('description_3e633a20')}{' '} + {rt( + 'authenticate_with_a_username_and_password_establishing_a38ff252', + )} +

    + + + + + {rt('parameter_9634fb08')} + + + {rt('required_11da9dc4')} + + {rt('type_ba40014f')} + {rt('description_4262c45d')} + + + + + + username + + {rt('yes_b5141d3d')} + string + {rt('username_1a3f0617')} + + + + password + + {rt('yes_b5141d3d')} + string + {rt('password_a621ab60')} + + + -

    - 2.3 退出登录 -

    -

    - 接口:GET{' '} - - /api/v1/user/logout - -

    -

    - 说明:销毁当前会话 Cookie 并退出登录状态。 -

    +

    + {rt('2_3_sign_out_3252611d')} +

    +

    + {rt('endpoint_79a2369a')} GET{' '} + + /api/v1/user/logout + +

    +

    + {rt('description_3e633a20')}{' '} + {rt('delete_the_current_session_cookie_and_sign_c213ffd9')} +

    -

    - 2.4 获取个人资料 -

    -

    - 接口:GET{' '} - - /api/v1/user/self - -

    -

    - 说明:获取当前登录账户的基本数据模型(包含 - ID、角色、昵称等)。 -

    -
    - ), - children: [ - { value: '2-1-register', title: '2.1 用户注册' }, - { value: '2-2-login', title: '2.2 密码登录' }, - { value: '2-3-logout', title: '2.3 退出登录' }, - { value: '2-4-profile', title: '2.4 获取个人资料' }, - ], - }, - { - value: 'token-apis', - title: '3. 个人访问令牌 (AccessToken) 接口', - content: ( -
    -
    -

    - AccessToken 管理相关接口均要求通过 Session - 登录后调用,支持普通用户权限。 +

    + {rt('2_4_get_profile_7b96110a')} +

    +

    + {rt('endpoint_79a2369a')} GET{' '} + + /api/v1/user/self + +

    +

    + {rt('description_3e633a20')}{' '} + {rt('get_the_signed_in_account_s_basic_b0eb8cfa')}

    + ), + children: [ + { value: '2-1-register', title: rt('2_1_user_registration_4f332f85') }, + { value: '2-2-login', title: rt('2_2_password_login_271483ba') }, + { value: '2-3-logout', title: rt('2_3_sign_out_3252611d') }, + { value: '2-4-profile', title: rt('2_4_get_profile_7b96110a') }, + ], + }, + { + value: 'token-apis', + title: rt('3_personal_access_token_endpoints_989f5d7c'), + content: ( +
    +
    +

    + {rt('access_token_management_requires_a_session_login_42d91543')} +

    +
    -

    - 3.1 获取令牌列表 -

    -

    - 接口:GET{' '} - - /api/v1/user/access-tokens - -

    -

    - 说明: - 查询当前用户已创建的所有令牌详情(令牌明文已被脱敏)。 -

    +

    + {rt('3_1_list_tokens_9840c227')} +

    +

    + {rt('endpoint_79a2369a')} GET{' '} + + /api/v1/user/access-tokens + +

    +

    + {rt('description_3e633a20')}{' '} + {rt('list_the_current_user_s_tokens_with_c34d2210')} +

    -

    - 3.2 新建访问令牌 -

    -

    - 接口:POST{' '} - - /api/v1/user/access-tokens - -

    -

    - 参数:JSON Body{' '} - {`{"name": "token名称", "is_admin": false}`} -

    -

    - 说明:生成一个全新访问令牌。返回体中包含一次性明文 - Token,切勿遗失。 -

    -

    - is_admin(可选,默认{' '} - false - ):是否赋予令牌管理员权限,仅管理员用户可设置。非管理员令牌无法访问{' '} - /admin/** 端点。 -

    -

    成功返回样例:

    - + {rt('3_2_create_an_access_token_e53bd784')} + +

    + {rt('endpoint_79a2369a')} POST{' '} + + /api/v1/user/access-tokens + +

    +

    + {rt('parameters_ba89685c')} JSON Body{' '} + + {rt.raw('name_token_name_is_admin_false_4f3e4868')} + +

    +

    + {rt('description_3e633a20')}{' '} + {rt('generate_an_access_token_the_response_includes_70552390')} +

    +

    + is_admin + {rt('optional_default_210b9d8f')}{' '} + false + {rt('grant_administrator_privileges_to_the_token_only_2292cad8')}{' '} + /admin/**{' '} + {rt('endpoints_815c88e5')} +

    +

    {rt('successful_response_example_7783a39c')}

    + + language='json' + /> -

    - 3.3 撤销/删除令牌 -

    -

    - 接口:DELETE{' '} - - /api/v1/user/access-tokens/:id - -

    -

    - 说明:通过 ID - 物理删除对应访问令牌,该令牌将立即失效。 -

    +

    + {rt('3_3_revoke_delete_a_token_ae510f58')} +

    +

    + {rt('endpoint_79a2369a')} DELETE{' '} + + /api/v1/user/access-tokens/:id + +

    +

    + {rt('description_3e633a20')}{' '} + {rt('permanently_delete_a_token_by_id_immediately_145287f6')} +

    -

    - 3.4 轮换令牌密钥 -

    -

    - 接口:POST{' '} - - /api/v1/user/access-tokens/:id/rotate - -

    -

    - 说明: - 轮换指定令牌的物理密钥值。系统将废弃原有密钥,返回新生成的明文 - Token,令牌名称与 ID 保持一致。 -

    -
    - ), - children: [ - { value: '3-1-list-token', title: '3.1 获取令牌列表' }, - { value: '3-2-create-token', title: '3.2 新建访问令牌' }, - { value: '3-3-delete-token', title: '3.3 撤销/删除令牌' }, - { value: '3-4-rotate-token', title: '3.4 轮换令牌密钥' }, - ], - }, - { - value: 'config-apis', - title: '4. 公共配置与管理接口', - content: ( -
    -

    - 4.1 公共系统配置 -

    -

    - 接口:GET{' '} - - /api/v1/config/public - -

    -

    - 说明: - 无感获取当前系统配置表中公共可见的键值集合。供前端页面动态渲染使用。 -

    -

    返回数据结构样例:

    - + {rt('3_4_rotate_a_token_secret_79209983')} + +

    + {rt('endpoint_79a2369a')} POST{' '} + + /api/v1/user/access-tokens/:id/rotate + +

    +

    + {rt('description_3e633a20')}{' '} + {rt('replace_a_token_s_secret_and_return_d2caca89')} +

    +
    + ), + children: [ + { value: '3-1-list-token', title: rt('3_1_list_tokens_9840c227') }, + { + value: '3-2-create-token', + title: rt('3_2_create_an_access_token_e53bd784'), + }, + { + value: '3-3-delete-token', + title: rt('3_3_revoke_delete_a_token_ae510f58'), + }, + { + value: '3-4-rotate-token', + title: rt('3_4_rotate_a_token_secret_79209983'), + }, + ], + }, + { + value: 'config-apis', + title: rt('4_public_configuration_and_administration_17ebdbd5'), + content: ( +
    +

    + {rt('4_1_public_system_configuration_2be912e4')} +

    +

    + {rt('endpoint_79a2369a')} GET{' '} + + /api/v1/config/public + +

    +

    + {rt('description_3e633a20')}{' '} + {rt('get_publicly_visible_configuration_keys_and_values_bc7bff66')} +

    +

    {rt('response_example_d3087445')}

    + + language='json' + /> -

    - 4.2 系统配置项 CRUD (管理员) -

    -

    - 说明:用于在后台对 `system_configs` - 配置进行动态变更,要求管理员权限会话调用。 -

    -
      -
    • - 获取配置列表:GET{' '} - - /api/v1/admin/system-configs?type=system - -
    • -
    • - 新建配置项:POST{' '} - - /api/v1/admin/system-configs - -
    • -
    • - 修改指定配置值:PUT{' '} - - /api/v1/admin/system-configs/:key - -
    • -
    • - 删除配置项:DELETE{' '} - - /api/v1/admin/system-configs/:key - -
    • -
    -
    - ), - children: [ - { value: '4-1-public-config', title: '4.1 公共系统配置' }, - { value: '4-2-admin-configs', title: '4.2 系统配置项 CRUD (管理员)' }, - ], - }, -]; +

    + {rt('4_2_system_configuration_crud_administrator_2ce66c8b')} +

    +

    + {rt('description_3e633a20')}{' '} + {rt( + 'manage_system_configs_dynamically_through_an_administrator_acdcdb99', + )} +

    +
      +
    • + {rt('list_configuration_3d58680b')} GET{' '} + + /api/v1/admin/system-configs?type=system + +
    • +
    • + {rt('create_a_configuration_entry_1d995264')}{' '} + POST{' '} + + /api/v1/admin/system-configs + +
    • +
    • + {rt('update_a_configuration_value_2a0b0b76')} PUT{' '} + + /api/v1/admin/system-configs/:key + +
    • +
    • + {rt('delete_a_configuration_entry_1b77a6ea')}{' '} + DELETE{' '} + + /api/v1/admin/system-configs/:key + +
    • +
    +
    + ), + children: [ + { + value: '4-1-public-config', + title: rt('4_1_public_system_configuration_2be912e4'), + }, + { + value: '4-2-admin-configs', + title: rt('4_2_system_configuration_crud_administrator_2ce66c8b'), + }, + ], + }, + ]; +} diff --git a/frontend/components/common/docs/how-to-use.tsx b/frontend/components/common/docs/how-to-use.tsx index ad3fd4f8..306c9088 100644 --- a/frontend/components/common/docs/how-to-use.tsx +++ b/frontend/components/common/docs/how-to-use.tsx @@ -1,3 +1,4 @@ +import type { RuntimeTranslator } from '@/i18n/runtime'; import { type PolicySection } from './types'; import { CodeBlock } from '@/components/ui/code-block'; @@ -6,249 +7,273 @@ import { CodeBlock } from '@/components/ui/code-block'; * 使用指南 (How To Use) * ------------------------------------------------------------------ */ -export const howToUseSections: PolicySection[] = [ - { - value: 'quick-start', - title: '1. 快速开始', - content: ( -
    -
    -

    - 为开发者提供通用全栈开发脚手架 (Boilerplate) 平台使用说明 -

    +export function createHowToUseSections(rt: RuntimeTranslator): PolicySection[] { + return [ + { + value: 'quick-start', + title: rt('1_quick_start_ef8fbb58'), + content: ( +
    +
    +

    + {rt('a_guide_to_the_general_purpose_full_9cd8c987')} +

    +
    +
      +
    • + {rt('architecture_e992b65c')}{' '} + {rt('go_gin_gorm_redis_asynq_backend_react_d5fe02d3')} +
    • +
    • + {rt('authentication_d36b345b')}{' '} + {rt( + 'local_username_password_registration_and_login_plus_583ab638', + )} +
    • +
    • + {rt('access_tokens_b7b856d3')}{' '} + {rt('personal_developer_access_tokens_api_keys_for_4b407149')} +
    • +
    • + {rt('observability_ea0cc1bf')}{' '} + {rt( + 'zap_structured_logging_and_opentelemetry_distributed_tracing_9fd6da9f', + )} +
    • +
    -
      -
    • - 架构底座:Go (Gin + GORM + Redis + Asynq) 后端 + - React (Next.js 16 + Tailwind CSS 4 + Shadcn UI) 前端 -
    • -
    • - 认证体系:支持本地常规账号密码注册登录 + - 第三方自定义 OIDC (OAuth2) 认证源绑定 -
    • -
    • - 访问令牌:提供开发者个人 AccessToken (API - Key),用于通过 Http Header 鉴权直接调用系统 API -
    • -
    • - 可观测性:集成 Zap 结构化日志与 OpenTelemetry - 全链路 Tracing 追踪 -
    • -
    -
    - ), - }, - { - value: 'auth-security', - title: '2. 身份认证与安全设置', - content: ( -
    -

    平台采用混合式身份认证,满足不同的部署和业务场景需求:

    -

    - 2.1 常规账号密码认证 -

    -
      -
    • - 自主注册与密码登录: - 支持普通用户通过用户名及密码直接进行注册与会话建立,密码在后端采用{' '} - - bcrypt - {' '} - 高强度加盐哈希存储。 -
    • -
    • - 系统开关控制: - 管理员可在后台配置动态开关,随时禁用自主密码注册或密码登录,以转为纯第三方认证模式。 -
    • -
    - -

    - 2.2 第三方 OIDC 认证源 -

    -

    用户可以在个人资料页面关联绑定外部授权账户:

    -
      -
    1. - 进入 设置 / 个人资料 页面。 -
    2. -
    3. - 在“第三方账号绑定”栏目下查看当前绑定的账号,或点击未绑定的可用 OIDC - 认证源直接触发 OAuth2 绑定流。 -
    4. -
    5. - 绑定成功后,用户在登录界面可直接点击 OIDC 登录按钮实现快捷跳转。 -
    6. -
    -
    - ), - children: [ - { value: '2-1-login', title: '2.1 常规账号密码认证' }, - { value: '2-2-oidc', title: '2.2 第三方 OIDC 认证源' }, - ], - }, - { - value: 'access-token', - title: '3. 个人访问令牌 (AccessToken) 接口对接', - content: ( -
    -

    - 为便于开发者或第三方工具直接调用系统 - API,平台提供个人访问令牌管理功能。 -

    -

    - 3.1 令牌生成与存储规范 -

    -
      -
    • - 一次性明文展示:创建令牌时生成的随机明文 Token 值 - (形如{' '} - - at_xxx - - ) - 仅会在弹窗中展示一次。请立即复制保存,关闭弹窗后系统将无法重新获取。 -
    • -
    • - 安全哈希存储:数据库仅存储 Token 的 SHA-256 - 哈希指纹,即使数据库泄漏,攻击者也无法通过摘要逆向恢复令牌原文。 -
    • -
    - -

    - 3.2 携带 Header 进行 API 调用 -

    -

    - 您可以凭借保存的明文 Token 随时调用系统开放接口,系统认证支持以下两种 - Http 请求头携带方式之一: -

    -
    -

    - 方式一:Authorization Bearer 头 + ), + }, + { + value: 'auth-security', + title: rt('2_authentication_and_security_settings_76967059'), + content: ( +

    +

    + {rt( + 'the_platform_combines_authentication_methods_to_support_bc9f1772', + )}

    - + {rt('2_1_username_and_password_authentication_97184f8f')} + +
      +
    • + {rt('registration_and_password_login_c5bd6cb4')}{' '} + {rt('users_can_register_and_create_sessions_with_21883e04')}{' '} + + bcrypt + {' '} + {rt('salted_hashing_dc2a9b63')} +
    • +
    • + {rt('system_switches_53741557')}{' '} + {rt( + 'administrators_can_disable_password_registration_or_login_a2648e62', + )} +
    • +
    + +

    + {rt('2_2_third_party_oidc_providers_f14083c8')} +

    +

    {rt('users_can_link_external_accounts_from_their_c08d3f5a')}

    +
      +
    1. + {rt('open_4540d74d')} + {rt('settings_profile_32835b0d')}{' '} + {rt('message_a173cce8')} +
    2. +
    3. + {rt( + 'under_linked_accounts_view_existing_connections_or_5151eb9b', + )} +
    4. +
    5. {rt('after_linking_use_the_oidc_login_button_6095b15d')}
    6. +
    +
    + ), + children: [ + { + value: '2-1-login', + title: rt('2_1_username_and_password_authentication_97184f8f'), + }, + { + value: '2-2-oidc', + title: rt('2_2_third_party_oidc_providers_f14083c8'), + }, + ], + }, + { + value: 'access-token', + title: rt('3_integrating_personal_access_tokens_af2647b5'), + content: ( +
    +

    + {rt('personal_access_tokens_allow_developers_and_third_9a242a0e')} +

    +

    + {rt('3_1_token_generation_and_storage_2b78c681')} +

    +
      +
    • + {rt('one_time_plaintext_display_fd13912a')}{' '} + {rt('a_newly_generated_plaintext_token_such_as_57eb11a1')}{' '} + + at_xxx + + {rt('is_displayed_only_once_copy_and_save_8d378c8a')} +
    • +
    • + {rt('secure_hash_storage_1ce392ff')}{' '} + {rt('only_a_sha_256_hash_of_the_dbf434b1')} +
    • +
    + +

    + {rt('3_2_calling_apis_with_headers_fccd7b0d')} +

    +

    {rt('use_your_saved_plaintext_token_with_either_3af53a05')}

    +
    +

    + {rt('method_1_authorization_bearer_header_2a9c2a6c')} +

    + -
    -
    -

    - 方式二:X-Access-Token 自定义头 -

    - +
    +
    +

    + {rt('method_2_custom_x_access_token_header_4548169f')} +

    + + language='http' + /> +
    -
    - ), - children: [ - { value: '3-1-generation', title: '3.1 令牌生成与存储规范' }, - { value: '3-2-usage', title: '3.2 携带 Header 进行 API 调用' }, - ], - }, - { - value: 'config-system', - title: '4. 动态系统配置项', - content: ( -
    -

    - 平台内置了完备的 KV 配置管理模块,允许管理员动态调整系统运行状态: -

    -
      -
    • - 缓存读取加速:配置加载基于 GORM 读取数据库,并辅以 - Redis Hash 结构进行多层缓存加速,大幅降低配置查询耗时。 -
    • -
    • - 核心系统配置项说明: -
        -
      • - - site_name - - :平台展示名称 -
      • -
      • - - password_login_enabled - - :密码登录开关 -
      • -
      • - - registration_enabled - - :用户自主注册开关 -
      • -
      • - - max_api_keys_per_user - - :普通用户创建令牌的最大数限制 (默认5) -
      • -
      -
    • -
    -
    - ), - }, - { - value: 'worker-scheduler', - title: '5. 异步任务与定时调度', - content: ( -
    -

    - 项目借助 Cobra 实现多命令入口分发,通过独立部署 Worker - 服务解耦复杂计算或高延迟IO: -

    -
      -
    • - 定时任务派发 (CMD scheduler):负责按 Cron - 表达式配置,定时将待执行任务推送到 Redis 队列中。 -
    • -
    • - 多优先级 Worker (CMD worker):基于 Asynq - 驱动,按优先级拉取任务并并发调度执行(如定时清理临时上传目录、同步外部系统日志等)。 -
    • -
    -
    - ), - }, - { - value: 'tracing-metrics', - title: '6. 链路追踪与结构化日志', - content: ( -
    -

    为了保障分布式微服务架构下的可观测性,平台接入了高级监控组件:

    -
      -
    • - OpenTelemetry Tracing:自动传递 Tracing - 上下文,所有经由 Gin 中间件、外部请求或 GORM - 数据库的事务操作都将带有全局唯一的 - Span,用于排查链路耗时或调用异常。 -
    • -
    • - Zap 结构化日志: - 将后端控制台或日志输出格式统一规范化为 JSON,方便与 ELK、Loki - 等日志收集分析工具无缝对接。 -
    • -
    -
    - ), - }, -]; + ), + children: [ + { + value: '3-1-generation', + title: rt('3_1_token_generation_and_storage_2b78c681'), + }, + { + value: '3-2-usage', + title: rt('3_2_calling_apis_with_headers_fccd7b0d'), + }, + ], + }, + { + value: 'config-system', + title: rt('4_dynamic_system_configuration_b3bf8c49'), + content: ( +
    +

    {rt('the_built_in_key_value_configuration_module_713f6d05')}

    +
      +
    • + {rt('cached_reads_35828322')}{' '} + {rt('configuration_is_read_from_the_database_through_3e9f6a34')} +
    • +
    • + {rt('core_system_configuration_keys_6dc02be8')} +
        +
      • + + site_name + + {rt('platform_display_name_116219b1')} +
      • +
      • + + password_login_enabled + + {rt('password_login_switch_153eaa45')} +
      • +
      • + + registration_enabled + + {rt('user_registration_switch_827b5b21')} +
      • +
      • + + max_api_keys_per_user + + {rt('maximum_tokens_per_regular_user_default_5_5dd6d9e7')} +
      • +
      +
    • +
    +
    + ), + }, + { + value: 'worker-scheduler', + title: rt('5_background_tasks_and_scheduling_7142ce96'), + content: ( +
    +

    + {rt('cobra_provides_separate_commands_so_worker_services_1bbae2df')} +

    +
      +
    • + + {rt('scheduled_dispatch_scheduler_command_e79062ad')} + {' '} + {rt('enqueues_tasks_in_redis_according_to_their_694e5985')} +
    • +
    • + + {rt('workers_with_queue_priorities_worker_command_faa970b1')} + {' '} + {rt('asynq_consumes_tasks_by_priority_and_executes_69d82251')} +
    • +
    +
    + ), + }, + { + value: 'tracing-metrics', + title: rt('6_distributed_tracing_and_structured_logs_e8971cad'), + content: ( +
    +

    + {rt( + 'the_platform_integrates_monitoring_components_for_observability_2b1da50f', + )} +

    +
      +
    • + OpenTelemetry Tracing:{' '} + {rt( + 'tracing_context_is_propagated_automatically_gin_requests_0f549cce', + )} +
    • +
    • + {rt('zap_structured_logs_1c9725e8')}{' '} + {rt('standardizes_backend_console_and_log_output_as_16970757')} +
    • +
    +
    + ), + }, + ]; +} diff --git a/frontend/components/common/docs/legal-page-layout.tsx b/frontend/components/common/docs/legal-page-layout.tsx index e549b401..1ab921ed 100644 --- a/frontend/components/common/docs/legal-page-layout.tsx +++ b/frontend/components/common/docs/legal-page-layout.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useTranslations } from 'next-intl'; import * as React from 'react'; import Link from 'next/link'; import { ArrowLeft, Calendar, Check, Copy, FileText } from 'lucide-react'; @@ -21,6 +22,8 @@ export function LegalPageLayout({ sections, description, }: LegalPageLayoutProps) { + const rt = useTranslations('runtime'); + const [activeSection, setActiveSection] = React.useState( sections[0]?.value || '', ); @@ -192,7 +195,7 @@ export function LegalPageLayout({ const lines: string[] = []; lines.push(`# ${title}`); lines.push(''); - lines.push(`最后更新:${lastUpdated}`); + lines.push(rt('last_updated_value0_87a896df', { value0: lastUpdated })); if (description) { const descriptionElement = contentRoot.querySelector( '[data-docs-description]', @@ -303,7 +306,7 @@ export function LegalPageLayout({ className='gap-2 pl-0 text-muted-foreground hover:text-foreground hover:bg-transparent -ml-2' > - 返回首页 + {rt('back_to_home_d66f4359')} @@ -318,11 +321,12 @@ export function LegalPageLayout({
    - {sections.length} 个内容 + {sections.length} {rt('sections_fcb2696e')} - 最后更新:{lastUpdated} + {rt('last_updated_a83cd480')} + {lastUpdated}
    {description && ( @@ -357,7 +363,7 @@ export function LegalPageLayout({
    - 目录 + {rt('table_of_contents_52daa71e')}