Skip to content

Conversation

@tbphp
Copy link
Owner

@tbphp tbphp commented Sep 7, 2025

关联 Issue / Related Issue

Closes #78

变更内容 / Change Content

  • Bug 修复 / Bug fix
  • 新功能 / New feature
  • 其他改动 / Other changes

自查清单 / Checklist

  • 我已在本地测试过我的变更。 / I have tested my changes locally.
  • 我已更新了必要的文档。 / I have updated the necessary documentation.

Summary by CodeRabbit

  • New Features

    • Added full internationalization across app and API with English, Japanese, and Chinese.
    • Introduced a language selector in layout and login.
    • API now respects Accept-Language and returns localized messages.
  • Improvements

    • Localized UI texts across dashboard, keys, logs, settings, charts, security alerts, theme toggle, footer, and notifications.
    • Settings metadata now displays translated names, categories, and descriptions.
    • Enhanced task and pagination messages with localized formats.
  • Chores

    • Added i18n libraries and updated dependencies to support localization.

@coderabbitai
Copy link

coderabbitai bot commented Sep 7, 2025

Walkthrough

Adds full-stack internationalization. Backend: introduces i18n bundle, locales (zh-CN, en-US, ja-JP), Gin middleware, response helpers, and wires i18n into app startup, router, handlers, and settings metadata. Frontend: integrates vue-i18n, locale selection, axios Accept-Language propagation, and replaces hard-coded strings across views/components. Updates dependencies accordingly.

Changes

Cohort / File(s) Summary
Go module updates
go.mod
Adds github.com/nicksnyder/go-i18n/v2 and updates golang.org/x/text; bumps golang.org/x/sync; removes older indirect x/text.
Backend i18n core
internal/i18n/i18n.go, internal/i18n/locales/*
New i18n bundle initialization, locale maps for zh-CN/en-US/ja-JP, localizer utilities, and translation function.
Backend i18n middleware & routing
internal/i18n/middleware.go, internal/router/router.go, internal/app/app.go
Adds Gin middleware to inject localizer/lang, app startup calls i18n.Init, router applies middleware for /api.
Response helpers (i18n)
internal/response/response.go
Adds SuccessI18n, ErrorI18n, ErrorI18nFromAPIError; default Success uses translated message.
Handlers migrated to i18n
internal/handler/dashboard_handler.go, .../group_handler.go, .../handler.go, .../key_handler.go, .../log_handler.go, .../settings_handler.go, .../task_handler.go
Replaces literals with i18n keys, uses new response helpers; some function signatures accept *gin.Context for translations.
Settings meta keying
internal/types/types.go
Changes struct tag metadata (name/category/desc) to config.* keys for runtime translation; no type changes.
Web i18n core
web/package.json, web/src/locales/index.ts, web/src/locales/zh-CN.ts, web/src/locales/en-US.ts, web/src/locales/ja-JP.ts, web/src/main.ts
Adds vue-i18n dependency, initializes i18n with 3 locales, locale detection/persistence, axios header wiring, mounts plugin.
Locale selector & layout
web/src/components/LanguageSelector.vue, web/src/components/Layout.vue, web/src/views/Login.vue
New language selector component; inserted into layout and login.
Frontend components localized
web/src/components/AppFooter.vue, .../BaseInfoCard.vue, .../GlobalTaskProgressBar.vue, .../LineChart.vue, .../Logout.vue, .../NavBar.vue, .../SecurityAlert.vue, .../ThemeToggle.vue, .../common/ProxyKeysInput.vue, .../keys/GroupCopyModal.vue, .../keys/GroupFormModal.vue, .../keys/GroupInfoCard.vue, .../keys/GroupList.vue, .../keys/KeyCreateDialog.vue, .../keys/KeyDeleteDialog.vue, .../keys/KeyTable.vue, .../logs/LogTable.vue, web/src/views/Settings.vue
Replaces hard-coded strings with t(...); introduces placeholders and interpolations; minor prop/event additions where noted.
Web API/util updates
web/src/api/keys.ts, web/src/api/logs.ts, web/src/utils/http.ts
Uses i18n for messages; sets Accept-Language header; standardizes success/error texts.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Browser as Frontend (Vue)
  participant I18N as Vue I18n
  participant Axios as Axios
  participant Server as Backend (Gin)
  participant MW as I18N Middleware
  participant Handlers as API Handlers
  participant GoI18N as go-i18n Bundle

  rect rgb(245,248,252)
    note over Browser,I18N: App bootstrap (new)
    Browser->>I18N: createI18n(locale from LS or browser)
    I18N-->>Browser: t() available
    Browser->>Axios: set default Accept-Language header
  end

  User->>Browser: Trigger API request
  Browser->>Axios: GET /api/...
  Axios->>Server: HTTP with Accept-Language
  Server->>MW: Process request (new)
  MW->>GoI18N: GetLocalizer(Accept-Language)
  MW-->>Server: Context set: localizer, lang
  Server->>Handlers: Handle endpoint
  Handlers->>GoI18N: Translate msg via localizer (T)
  Handlers-->>Server: Response payload with localized message
  Server-->>Axios: JSON { message, lang, data }
  Axios-->>Browser: Response
  Browser->>I18N: Render UI with t() strings
Loading
sequenceDiagram
  autonumber
  participant App as App.Start()
  participant I18N as internal/i18n.Init (new)
  participant Log as logrus

  App->>I18N: Init()
  I18N-->>App: error? (if load fails)
  alt init failed
    App-->>App: return error "failed to initialize i18n"
  else init ok
    App->>Log: Info "i18n initialized successfully."
    App-->>App: continue startup
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

  • feat: Security-hardening Optimization #225 — Introduces security warning/password checks in dashboard handlers; this PR refactors those paths to accept context and localize their outputs, touching the same functions.

Pre-merge checks (3 passed, 2 warnings)

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The pull request includes functional additions unrelated to internationalization, such as extending the VersionInfo model in web/src/services/version.ts and adding reactive watchers plus a new “copy-success” event emission in GroupInfoCard.vue, which fall outside the multi-language support objectives of issue #78. Please remove or relocate the VersionInfo model updates and the GroupInfoCard reactive behavior changes into separate PRs so that this PR focuses solely on implementing i18n support.
Description Check ⚠️ Warning The pull request description omits the required detailed summary of the changes beneath the Change Content checklist, leaving the template’s “Please describe your changes in detail below” section empty. Please add a concise high-level summary and detailed description of the implemented internationalization changes to satisfy the “Change Content” section of the PR template and provide context for reviewers.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title “feat: Internationalization” concisely and accurately reflects the primary change of this pull request by indicating the addition of internationalization support across the codebase.
Linked Issues Check ✅ Passed The changes comprehensively implement the multi-language internationalization support specified in issue #78 by integrating go-i18n in the backend, adding language bundles and middleware, and wiring up vue-i18n in the frontend with locale files for zh-CN, en-US, and ja-JP.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Poem

A rabbit taps in triple tongues, hooray!
From zh-CN to en, then ja—OK!
Headers whisper which words to say,
Charts and keys now sing en trois.
Hop, translate, and bound away—
Multilingual burrows, bright as day. 🐇🌐✨

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-internationalization

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (11)
web/src/components/BaseInfoCard.vue (1)

171-177: Guard optional chaining on error_rate

stats?.error_rate.trend can throw if error_rate is undefined. Chain the property access.

Apply:

-              <n-tag
-                v-if="stats?.error_rate.trend !== 0"
+              <n-tag
+                v-if="stats?.error_rate?.trend !== 0"
                 :type="stats?.error_rate.trend_is_growth ? 'success' : 'error'"

Optional: also chain trend_is_growth for consistency:

-                :type="stats?.error_rate.trend_is_growth ? 'success' : 'error'"
+                :type="stats?.error_rate?.trend_is_growth ? 'success' : 'error'"
web/src/components/logs/LogTable.vue (4)

119-125: Date formatting hard-coded to zh-CN

Use current i18n locale so dates localize correctly.

-const formatDateTime = (timestamp: string) => {
+const { t, locale } = useI18n();
+const formatDateTime = (timestamp: string) => {
   if (!timestamp) {
     return "-";
   }
   const date = new Date(timestamp);
-  return date.toLocaleString("zh-CN", { hour12: false }).replace(/\//g, "-");
+  return date
+    .toLocaleString(locale.value, { hour12: false })
+    .replace(/\//g, "-");
};

212-231: Localize the “Key” column header

Keep table headers fully translated.

-  {
-    title: "Key",
+  {
+    title: t("logs.key"),
     key: "key_value",
     width: 200,

609-616: Localize “User Agent” label and copy text

Use existing translation key logs.userAgent.

-                  <span class="compact-field-title">User Agent</span>
+                  <span class="compact-field-title">{{ t("logs.userAgent") }}</span>
@@
-                    @click="copyContent(selectedLog.user_agent, 'User Agent')"
+                    @click="copyContent(selectedLog.user_agent, t('logs.userAgent'))"

255-256: Rebuild columns on locale change

Static columns won’t reflect new translations after switching language.

-const columns = createColumns();
+const columns = computed(() => createColumns());
web/src/components/AppFooter.vue (1)

31-53: Make status texts reactive to locale changes.

statusConfig.text is computed once; switching locale won’t update the footer labels.

Apply this diff for the block to make it reactive:

-// 版本状态配置
-const statusConfig = {
+// 版本状态配置(texts reactive to locale)
+const statusConfig = computed(() => ({
   checking: {
     color: "#0066cc",
     icon: TimeOutline,
-    text: t("footer.checking"),
+    text: t("footer.checking"),
   },
   latest: {
     color: "#18a058",
     icon: CheckmarkCircleOutline,
-    text: t("footer.latestVersion"),
+    text: t("footer.latestVersion"),
   },
   "update-available": {
     color: "#f0a020",
     icon: WarningOutline,
-    text: t("footer.updateAvailable"),
+    text: t("footer.updateAvailable"),
   },
   error: {
     color: "#d03050",
     icon: WarningOutline,
-    text: t("footer.checkFailed"),
+    text: t("footer.checkFailed"),
   },
-};
+}));

And update usages in template to account for the computed (template auto-unwraps .value, so no further changes needed). Also ensure computed is imported (outside the selected range):

import { onMounted, ref, computed } from "vue";
web/src/components/GlobalTaskProgressBar.vue (1)

107-109: Bug: clearInterval used for a setTimeout handle.
Timer won’t be cleared reliably. Use clearTimeout.

-  if (pollTimer) {
-    clearInterval(pollTimer);
-    pollTimer = null;
-  }
+  if (pollTimer) {
+    clearTimeout(pollTimer);
+    pollTimer = null;
+  }
web/src/components/SecurityAlert.vue (1)

114-117: Block reverse‑tabnabbing on docs link (and optionally localize URL).

Opening a new tab without noopener/noreferrer can expose window.opener. Add the feature string; optionally append current locale to the docs URL.

-  window.open("https://www.gpt-load.com/docs/configuration/security", "_blank");
+  window.open("https://www.gpt-load.com/docs/configuration/security", "_blank", "noopener,noreferrer");

If you want locale-aware docs:

// outside selected range
// const { t } = useI18n();
const { t, locale } = useI18n();
// ...
const openSecurityDocs = () => {
  const url = `https://www.gpt-load.com/docs/configuration/security?lang=${encodeURIComponent(locale.value)}`;
  window.open(url, "_blank", "noopener,noreferrer");
};
web/src/components/common/ProxyKeysInput.vue (1)

34-41: Use cryptographically secure RNG for key generation.

Math.random is predictable. Use Web Crypto for security of generated keys.

-function generateRandomString(length: number): string {
-  const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
-  let result = "";
-  for (let i = 0; i < length; i++) {
-    result += chars.charAt(Math.floor(Math.random() * chars.length));
-  }
-  return result;
-}
+function generateRandomString(length: number): string {
+  const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+  const bytes = new Uint8Array(length);
+  crypto.getRandomValues(bytes);
+  let result = "";
+  for (let i = 0; i < length; i++) {
+    // 256 % 64 === 0 => uniform mapping
+    result += chars[bytes[i] % 64];
+  }
+  return result;
+}
web/src/components/keys/GroupFormModal.vue (1)

439-516: Block submit on duplicate header keys and show feedback; add success toast.

Avoid sending conflicting header rules; confirm success to user.

 async function handleSubmit() {
   if (loading.value) {
     return;
   }
 
   try {
-    await formRef.value?.validate();
+    await formRef.value?.validate();
+    // prevent duplicate header keys (canonical form)
+    const hasDup = formData.header_rules.some((_, idx) =>
+      !validateHeaderKeyUniqueness(formData.header_rules, idx, formData.header_rules[idx].key)
+    );
+    if (hasDup) {
+      message.error(t("keys.duplicateHeader"));
+      return;
+    }
 
     loading.value = true;
@@
     if (props.group?.id) {
       // 编辑模式
       res = await keysApi.updateGroup(props.group.id, submitData);
     } else {
       // 新建模式
       res = await keysApi.createGroup(submitData);
     }
 
     emit("success", res);
+    message.success(props.group?.id ? t("common.updatedSuccessfully") : t("common.createdSuccessfully"));
web/src/components/keys/GroupInfoCard.vue (1)

8-24: Fix missing Naive UI component imports (NStatistic, NGradientText, NDivider).

These components are used in the template but not imported, which will trigger “Failed to resolve component” at runtime.

Apply:

 import {
   NButton,
   NButtonGroup,
   NCard,
   NCollapse,
   NCollapseItem,
+  NDivider,
   NForm,
   NFormItem,
   NGrid,
   NGridItem,
   NIcon,
   NInput,
+  NStatistic,
+  NGradientText,
   NSpin,
   NTag,
   NTooltip,
   useDialog,
 } from "naive-ui";

Also applies to: 351-396

🧹 Nitpick comments (45)
web/package.json (2)

31-31: Fix npm script chaining

"npm lint:check" won’t run; use "npm run ...".

Apply:

-"check-all": "npm lint:check && npm format:check && npm type-check"
+"check-all": "npm run lint:check && npm run format:check && npm run type-check"

39-39: Enable build-time message precompilation with matching @intlify/unplugin-vue-i18n
Add the @intlify/unplugin-vue-i18n plugin to your Vite setup for precompiling i18n messages.

  • If you stay on vue-i18n v9 (Vue 3.5.x), use the v4 or v5 series of @intlify/unplugin-vue-i18n (these align with the v9 API); be aware of reported build errors when mixing with Vite 6.x.
  • For full compatibility with Vite 6.x and the latest bundler improvements, upgrade to vue-i18n v10 and install @intlify/unplugin-vue-i18n v6 (or the newest major) before enabling the plugin in vite.config.js.
    Verify your chosen versions against your current Vite version to avoid mismatches.
web/src/locales/en-US.ts (1)

1-1: Add const assertion for stronger typing

This improves key inference and guards against accidental mutations.

Apply:

-};
+} as const;

Also applies to: 561-561

web/src/locales/zh-CN.ts (3)

207-215: Terminology consistency: “1小时请求” → “近1小时请求”

Align with adjacent labels like “近1小时失败请求/失败率”.

Apply:

-    hourlyRequests: "1小时请求",
+    hourlyRequests: "近1小时请求",

278-279: Use consistent term for “密钥”

Prefer “密钥” over “key” in user-facing Chinese text.

-    optionalCustomValidationPath: "可选,自定义用于验证key的API路径",
+    optionalCustomValidationPath: "可选,自定义用于验证密钥的 API 路径",

329-331: Chinese/English mix: “Key 精确匹配”

Use full Chinese to avoid mixing.

-    keyExactMatch: "Key 精确匹配",
+    keyExactMatch: "密钥精确匹配",
web/src/locales/ja-JP.ts (2)

96-97: More natural “About” label

“について” is terse; “このアプリについて” reads better.

-    about: "について",
+    about: "このアプリについて",

189-191: Improve “Retry request” phrasing

“リトライリクエスト” → “再試行リクエスト”.

-    testSuccess: "キーテスト成功(所要時間: {duration})",
-    testFailed: "キーテスト失敗: 無効なAPIキー",
+    testSuccess: "キーテスト成功(所要時間: {duration})",
+    testFailed: "キーテスト失敗: 無効なAPIキー",

And for request type labels:

-    retryRequest: "リトライリクエスト",
+    retryRequest: "再試行リクエスト",
web/src/components/logs/LogTable.vue (1)

106-114: Unify message API usage

You use both window.$message and useMessage(); prefer one (ideally useMessage) for consistency/testability.

-      window.$message.error(res.message || t("logs.loadFailed"), {
+      message.error(res.message || t("logs.loadFailed"), {
         keepAliveOnHover: true,
         duration: 5000,
         closable: true,
       });
@@
-    window.$message.error(t("logs.requestFailed"));
+    message.error(t("logs.requestFailed"));
internal/router/router.go (1)

86-88: Attach “Vary: Accept-Language” for localized responses

Add a Vary header (ideally in i18n.Middleware) to play nicely with caches/CDNs when content varies by language.

 api := router.Group("/api")
-	api.Use(i18n.Middleware())
+	api.Use(i18n.Middleware())
+	// Optionally ensure Vary header if not already set in middleware
+	api.Use(func(c *gin.Context) {
+		c.Header("Vary", "Accept-Language")
+		c.Next()
+	})
web/src/components/NavBar.vue (1)

5-8: Make translations reactive to locale switches (compute at render-time).

You're capturing translated strings in menuOptions once; depending on vue-i18n reactivity, labels may not refresh on locale change. Prefer passing the i18n key and calling t(...) inside the label render fn.

Apply this diff within the provided range:

-    renderMenuItem("dashboard", t("nav.dashboard"), "📊"),
-    renderMenuItem("keys", t("nav.keys"), "🔑"),
-    renderMenuItem("logs", t("nav.logs"), "📋"),
-    renderMenuItem("settings", t("nav.settings"), "⚙️"),
+    renderMenuItem("dashboard", "nav.dashboard", "📊"),
+    renderMenuItem("keys", "nav.keys", "🔑"),
+    renderMenuItem("logs", "nav.logs", "📋"),
+    renderMenuItem("settings", "nav.settings", "⚙️"),

And update renderMenuItem accordingly (outside the selected range):

function renderMenuItem(key: string, labelKey: string, icon: string): MenuOption {
  return {
    label: () =>
      h(
        RouterLink,
        { to: { name: key }, class: "nav-menu-item" },
        { default: () => [h("span", { class: "nav-item-icon" }, icon), h("span", { class: "nav-item-text" }, t(labelKey))] }
      ),
    key,
  };
}

Also applies to: 18-23

web/src/components/keys/KeyCreateDialog.vue (1)

98-100: Disable “Create” when input is whitespace-only.

Button uses !keysText which treats " " as enabled. Trim to match submit guard.

-          <n-button type="primary" @click="handleSubmit" :loading="loading" :disabled="!keysText">
+          <n-button type="primary" @click="handleSubmit" :loading="loading" :disabled="!keysText?.trim()">
web/src/views/Login.vue (1)

71-79: Prevent empty submissions at the UI level.

Disable the login button when there’s no auth key to reduce needless clicks.

-            :disabled="loading"
+            :disabled="loading || !authKey"
web/src/components/AppFooter.vue (1)

69-71: Reflect failures in UI state.

On exception, set versionInfo.status = "error" so users see “check failed.”

   } catch (error) {
     console.warn("Version check failed:", error);
+    versionInfo.value.status = "error";
   } finally {
web/src/components/keys/KeyDeleteDialog.vue (2)

99-101: Prevent enabling “Delete” when input is only whitespace.
Currently :disabled="!keysText" becomes truthy for spaces. Gate on trimmed content.

-          <n-button type="error" @click="handleSubmit" :loading="loading" :disabled="!keysText">
+          <n-button type="error" @click="handleSubmit" :loading="loading" :disabled="!canSubmit">
             {{ t("common.delete") }}
           </n-button>

Additions outside this hunk:

// import line
import { ref, watch, computed } from "vue";

// below keysText
const canSubmit = computed(() => keysText.value.trim().length > 0 && !loading.value);

62-62: Avoid double success toasts (component + interceptor).
This component shows a success toast, and http interceptor also shows a generic success for non-GET. Suppress one of them (prefer interceptor or pass hideMessage to this request).

Examples:

  • Pass hideMessage in API layer for this call.
  • Or change interceptor to only toast when server returns a message (see http.ts comment).
web/src/components/GlobalTaskProgressBar.vue (1)

7-10: Localize numbers in messages (use i18n number formatter).
Show locale-formatted counts in completion toasts.

-import { useI18n } from "vue-i18n";
+import { useI18n } from "vue-i18n";
 
-const { t } = useI18n();
+const { t, n } = useI18n();
-            msg = t("task.validationCompleted", {
-              total: result.total_keys,
-              valid: result.valid_keys,
-              invalid: result.invalid_keys,
-            });
+            msg = t("task.validationCompleted", {
+              total: n(result.total_keys),
+              valid: n(result.valid_keys),
+              invalid: n(result.invalid_keys),
+            });
-            msg = t("task.importCompleted", {
-              added: result.added_count,
-              ignored: result.ignored_count,
-            });
+            msg = t("task.importCompleted", {
+              added: n(result.added_count),
+              ignored: n(result.ignored_count),
+            });
-            msg = t("task.deleteCompleted", {
-              deleted: result.deleted_count,
-              ignored: result.ignored_count,
-            });
+            msg = t("task.deleteCompleted", {
+              deleted: n(result.deleted_count),
+              ignored: n(result.ignored_count),
+            });

Also applies to: 55-59, 62-65, 68-71

web/src/components/ThemeToggle.vue (1)

6-9: Make tooltip text reactive to locale changes.
t(...) inside a computed that only depends on themeMode won’t recompute on locale switch. Track locale.

-import { useI18n } from "vue-i18n";
+import { useI18n } from "vue-i18n";
 
-const { t } = useI18n();
+const { t, locale } = useI18n();
 
 // 根据当前主题模式计算图标和提示文字
 const themeConfig = computed(() => {
+  // depend on locale so texts update when language changes
+  void locale.value;
   switch (themeMode.value) {
     case "auto":
       return {
         icon: Contrast,
         tooltip: t("theme.auto"),
         nextMode: t("theme.light"),
       };
     case "light":
       return {
         icon: Sunny,
         tooltip: t("theme.light"),
         nextMode: t("theme.dark"),
       };
     case "dark":
       return {
         icon: Moon,
         tooltip: t("theme.dark"),
         nextMode: t("theme.auto"),
       };
     default:
       return {
         icon: Contrast,
         tooltip: t("theme.auto"),
         nextMode: t("theme.light"),
       };
   }
 });

Also applies to: 11-38

web/src/utils/http.ts (2)

31-34: Use the app’s current locale as default for Accept-Language.
Align header with i18n state when localStorage is empty.

-  // 添加语言头
-  const locale = localStorage.getItem("locale") || "zh-CN";
-  config.headers["Accept-Language"] = locale;
+  // 添加语言头(优先 localStorage,其次全局 i18n)
+  const locale =
+    localStorage.getItem("locale") ||
+    i18n.global.locale.value ||
+    navigator.language ||
+    "zh-CN";
+  config.headers["Accept-Language"] = locale;

41-43: Reduce duplicate toasts by only showing server-provided messages
Normalize response.config.method to lowercase and require a non-empty response.data.message before calling the toast:

-    if (response.config.method !== "get" && !response.config.hideMessage) {
-      window.$message.success(response.data.message ?? i18n.global.t("common.operationSuccess"));
-    }
+    const method = response.config.method?.toLowerCase();
+    const serverMsg = response.data?.message;
+    if (!response.config.hideMessage && method !== "get" && serverMsg) {
+      window.$message.success(serverMsg);
+    }

Ensure every API call that already shows its own custom toast (e.g. deleteKeysAsync) sets hideMessage: true; verify the same for any import/validate key endpoints.

web/src/components/SecurityAlert.vue (1)

28-28: Guard localStorage for SSR/private mode.

Access can throw in SSR or locked storage contexts. Read lazily and wrap writes.

-import { computed, ref } from "vue";
+import { computed, ref, onMounted } from "vue";
-const isDismissedPermanently = ref(localStorage.getItem(STORAGE_KEY) === "true");
+const isDismissedPermanently = ref(false);
+onMounted(() => {
+  try {
+    isDismissedPermanently.value = localStorage.getItem(STORAGE_KEY) === "true";
+  } catch {}
+});
-  localStorage.setItem(STORAGE_KEY, "true");
+  try { localStorage.setItem(STORAGE_KEY, "true"); } catch {}

Also applies to: 109-112

web/src/components/LanguageSelector.vue (2)

31-37: Add accessible label to the language button.

Improves screen reader UX.

-    <n-button quaternary size="medium" class="language-selector-btn">
+    <n-button quaternary size="medium" class="language-selector-btn" :aria-label="t('common.changeLanguage')">

1-27: Consider wiring Naive UI i18n to follow app locale.

Hook NConfigProvider locale/date-locale to your current locale so component texts (calendar, pagination, etc.) localize too.

web/src/views/Settings.vue (1)

56-69: Surface localized success/error toasts on save.

Current flow silently succeeds/fails. Show feedback.

 async function handleSubmit() {
   if (isSaving.value) {
     return;
   }
 
   try {
     await formRef.value.validate();
     isSaving.value = true;
     await settingsApi.updateSettings(form.value);
     await fetchSettings();
+    message.success(t("settings.savedSuccessfully"));
+  } catch (_e) {
+    message.error(t("settings.saveFailed"));
   } finally {
     isSaving.value = false;
   }
 }
web/src/components/LineChart.vue (3)

79-86: Make time/number formatting locale-aware.

Use app locale for axis labels and values.

-import { useI18n } from "vue-i18n";
+import { useI18n } from "vue-i18n";
-const { t } = useI18n();
+const { t, locale } = useI18n();
-  return date.toLocaleTimeString(undefined, {
+  return date.toLocaleTimeString(locale.value, {
     hour: "2-digit",
     minute: "2-digit",
     hour12: false,
   });
-const formatNumber = (value: number) => {
-  if (value >= 1000) {
-    return `${(value / 1000).toFixed(1)}K`;
-  }
-  return Math.round(value).toString();
-};
+const formatNumber = (value: number) => {
+  return new Intl.NumberFormat(locale.value, {
+    notation: "compact",
+    maximumFractionDigits: 1,
+  }).format(value);
+};

Also applies to: 206-214


216-218: Error-series detection: case-insensitive and broader locale coverage.

Covers lowercase “error” and common zh/ja variants.

-const isErrorDataset = (label: string) => {
-  return label.includes("失败") || label.includes("Error") || label.includes("エラー");
-};
+const isErrorDataset = (label: string) => {
+  const l = label.toLowerCase();
+  return (
+    l.includes("error") ||
+    l.includes("失败") || // zh-CN
+    l.includes("錯誤") || // zh-TW
+    l.includes("失敗") || // ja (failure)
+    label.includes("エラー") // ja (error, Katakana)
+  );
+};

327-341: Keep “All groups” option in sync with locale changes.

This label is computed once in fetch; update on locale change.

// outside selected range
import { watch } from "vue";
const { t, locale } = useI18n();
watch(locale, () => {
  if (groupOptions.value.length) {
    groupOptions.value[0].label = t("charts.allGroups");
  }
});
web/src/components/keys/GroupCopyModal.vue (1)

70-72: Localize the “_copy” suffix in previewed group name.

End-users in non-English locales may prefer a localized suffix.

-  return `${baseName}_copy`;
+  return `${baseName}${t("keys.copySuffix")}`;
web/src/locales/index.ts (2)

44-55: Set initial document lang attribute.

Ensure reflects the resolved locale on boot.

 const defaultLocale = getDefaultLocale();
 const i18n = createI18n({
   legacy: false, // 使用 Composition API 模式
   locale: defaultLocale,
   fallbackLocale: "zh-CN",
   messages: {
     "zh-CN": zhCN,
     "en-US": enUS,
     "ja-JP": jaJP,
   },
 });
+if (typeof document !== "undefined") {
+  document.documentElement.setAttribute("lang", defaultLocale);
+}

61-72: Validate locale input in setLocale.

Avoid setting unsupported locales accidentally.

 export function setLocale(locale: Locale) {
+  if (!SUPPORTED_LOCALES.some(l => l.key === locale)) return;
   // 使用响应式的 locale.value 实现即时切换
   i18n.global.locale.value = locale;
   localStorage.setItem("locale", locale);
   document.documentElement.setAttribute("lang", locale);
internal/i18n/i18n.go (2)

59-80: Honor full Accept-Language with quality values and fallback order

Parse all tags instead of taking only the first; preserve user preference ordering.

 func parseAcceptLanguage(acceptLang string) []string {
 	if acceptLang == "" {
 		return nil
 	}
-	
-	// 简单解析,只取第一个语言
-	parts := strings.Split(acceptLang, ",")
-	if len(parts) > 0 {
-		lang := strings.TrimSpace(parts[0])
-		// 移除质量因子 (q=...)
-		if idx := strings.Index(lang, ";"); idx > 0 {
-			lang = lang[:idx]
-		}
-		
-		// 标准化语言代码
-		lang = normalizeLanguageCode(lang)
-		return []string{lang}
-	}
-	
-	return nil
+
+	tags, _, err := language.ParseAcceptLanguage(acceptLang)
+	if err != nil || len(tags) == 0 {
+		return nil
+	}
+	out := make([]string, 0, len(tags))
+	for _, tag := range tags {
+		out = append(out, normalizeLanguageCode(tag.String()))
+	}
+	return out
 }

110-127: DefaultMessage fallback for unknown IDs

Returning msgID is fine; you can also set DefaultMessage to keep formatting consistent when keys are missing.

 	config := &i18n.LocalizeConfig{
-		MessageID: msgID,
+		MessageID:      msgID,
+		DefaultMessage: &i18n.Message{ID: msgID, Other: msgID},
 	}
internal/i18n/middleware.go (4)

15-33: Propagate resolved language and set Content-Language header

Use the same parsed order as GetLocalizer and expose the chosen language via HTTP header.

 func Middleware() gin.HandlerFunc {
 	return func(c *gin.Context) {
 		// 获取 Accept-Language 头
 		acceptLang := c.GetHeader("Accept-Language")
 		
 		// 获取 Localizer
 		localizer := GetLocalizer(acceptLang)
 		
 		// 将 Localizer 存储到 Context 中
 		c.Set(LocalizerKey, localizer)
 		
 		// 存储当前语言
-		lang := normalizeLanguageCode(acceptLang)
-		c.Set(LangKey, lang)
+		langs := parseAcceptLanguage(acceptLang)
+		lang := "zh-CN"
+		if len(langs) > 0 {
+			lang = langs[0]
+		}
+		c.Set(LangKey, lang)
+		c.Header("Content-Language", lang)
 		
 		c.Next()
 	}
 }

Add import:

 import (
+	"net/http"
 	"github.com/gin-gonic/gin"
 	"github.com/nicksnyder/go-i18n/v2/i18n"
 )

56-68: Use http.StatusOK for clarity and consistency

Avoid magic numbers for status codes.

-	c.JSON(200, gin.H{
+	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": message,
 		"data":    data,
 		"lang":    GetLangFromContext(c),
 	})

69-80: Ditto: SuccessWithData should also use http.StatusOK

-	c.JSON(200, gin.H{
+	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": message,
 		"data":    data,
 		"lang":    GetLangFromContext(c),
 	})

82-105: Consider including the original msgID in error payloads for client-side fallbacks

Optional: add "messageID" along with localized "message" to let clients map or re-localize if needed.

Example:

 	c.JSON(code, gin.H{
 		"success": false,
 		"message": message,
+		"messageID": msgID,
 		"lang":    GetLangFromContext(c),
 	})
web/src/components/keys/KeyTable.vue (4)

29-31: Import computed to support reactive i18n options

-import { h, ref, watch } from "vue";
+import { h, ref, watch, computed } from "vue";

262-283: Consider i18n for duration units (m/s/ms)

Duration formatting is hard-coded in English. If you want full i18n, expose minutes/seconds/ms via t() or Intl.NumberFormat/Intl.RelativeTimeFormat.


88-90: Typo: isDeling → isDeleting

Minor naming fix improves readability.

-const isDeling = ref(false);
+const isDeleting = ref(false);

Also update its usages accordingly in this file.


191-208: Initialize is_visible to avoid type assertions on casted results

When assigning API results, add a map to ensure every row has is_visible set, preventing undefined checks in the template.

-keys.value = result.items as KeyRow[];
+keys.value = (result.items as APIKey[]).map(k => ({ ...k, is_visible: false })) as KeyRow[];
web/src/components/keys/GroupInfoCard.vue (5)

253-269: Use locale-aware number and percent formatting.

Hardcoded K/% aren’t localized. Use Intl.NumberFormat with the active locale.

Apply:

-const { t } = useI18n();
+const { t, locale } = useI18n();
 function formatNumber(num: number): string {
-  // if (num >= 1000000) {
-  //   return `${(num / 1000000).toFixed(1)}M`;
-  // }
-  if (num >= 1000) {
-    return `${(num / 1000).toFixed(1)}K`;
-  }
-  return num.toString();
+  return new Intl.NumberFormat(locale.value, {
+    notation: "compact",
+    maximumFractionDigits: 1,
+  }).format(num ?? 0);
 }
 
 function formatPercentage(num: number): string {
-  if (num <= 0) {
-    return "0";
-  }
-  return `${(num * 100).toFixed(1)}%`;
+  return new Intl.NumberFormat(locale.value, {
+    style: "percent",
+    minimumFractionDigits: 0,
+    maximumFractionDigits: 1,
+  }).format(Math.max(0, num ?? 0));
 }

Also applies to: 30-31


354-355: Avoid hardcoded punctuation and concatenated labels; move to i18n with placeholders.

Embedding the full-width colon “:” and concatenating values limits localization flexibility.

Apply pattern (repeat for daily/weekly):

-<n-statistic :label="`${t('keys.keyCount')}:${stats?.key_stats?.total_keys ?? 0}`">
+<n-statistic :label="t('keys.keyCountLabel', { total: stats?.key_stats?.total_keys ?? 0 })">
-:label="`${t('keys.hourlyRequests')}:${formatNumber(stats?.hourly_stats?.total_requests ?? 0)}`"
+:label="t('keys.hourlyRequestsLabel', { total: formatNumber(stats?.hourly_stats?.total_requests ?? 0) })"

Ensure locale files define:

  • keys.keyCountLabel: e.g., "Keys: {total}"
  • keys.hourlyRequestsLabel, keys.dailyRequestsLabel, keys.weeklyRequestsLabel

Also applies to: 376-377, 399-400, 422-423


210-217: Unify delete-confirm copy into a single translatable sentence.

Concatenating multiple t() fragments around a styled name is fragile across languages.

Apply:

-        content: () =>
-          h("div", null, [
-            h("p", null, [
-              t("keys.dangerousOperation"),
-              h("strong", { style: { color: "#d03050" } }, props.group?.name),
-              t("keys.toConfirmDeletion"),
-            ]),
+        content: () =>
+          h("div", null, [
+            h("p", null, t("keys.deleteConfirmMessage", { name: props.group?.name })),
             h(NInput, {

Add keys.deleteConfirmMessage in locales, e.g., “This is dangerous. Enter {name} to confirm deletion.” If you must keep highlighting, render via an i18n-t slot instead of concatenation.


156-163: Surface a user-facing error when loading config options fails.

Right now only console.error is logged.

Apply:

 async function loadConfigOptions() {
   try {
     const options = await keysApi.getGroupConfigOptions();
     configOptions.value = options || [];
   } catch (error) {
     console.error("Failed to load config options:", error);
+    window.$message.error(t("keys.loadConfigOptionsFailed"));
   }
 }

Remember to add keys.loadConfigOptionsFailed to locales.


165-173: Optional: i18n fallback for config option name/description.

Try localized strings by convention before falling back to server-provided fields.

Apply:

 function getConfigDisplayName(key: string): string {
-  const option = configOptions.value.find(opt => opt.key === key);
-  return option?.name || key;
+  const option = configOptions.value.find(opt => opt.key === key);
+  const k = `keys.config.${key}.name`;
+  // Prefer i18n, else API name, else raw key
+  return (/* @ts-ignore */ t.te?.(k) ? t(k) : option?.name) || key;
 }
 
 function getConfigDescription(key: string): string {
-  const option = configOptions.value.find(opt => opt.key === key);
-  return option?.description || t("keys.noDescription");
+  const option = configOptions.value.find(opt => opt.key === key);
+  const k = `keys.config.${key}.description`;
+  return (/* @ts-ignore */ t.te?.(k) ? t(k) : option?.description) || t("keys.noDescription");
 }

If you prefer strict typing, grab te from useI18n(): const { t, te, locale } = useI18n(); and replace the inline t.te usage.

Also applies to: 570-571

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ea2d20 and 366043f.

⛔ Files ignored due to path filters (2)
  • go.sum is excluded by !**/*.sum
  • web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (35)
  • go.mod (2 hunks)
  • internal/app/app.go (2 hunks)
  • internal/i18n/i18n.go (1 hunks)
  • internal/i18n/middleware.go (1 hunks)
  • internal/router/router.go (2 hunks)
  • web/package.json (1 hunks)
  • web/src/api/keys.ts (2 hunks)
  • web/src/api/logs.ts (2 hunks)
  • web/src/components/AppFooter.vue (7 hunks)
  • web/src/components/BaseInfoCard.vue (6 hunks)
  • web/src/components/GlobalTaskProgressBar.vue (4 hunks)
  • web/src/components/LanguageSelector.vue (1 hunks)
  • web/src/components/Layout.vue (3 hunks)
  • web/src/components/LineChart.vue (6 hunks)
  • web/src/components/Logout.vue (2 hunks)
  • web/src/components/NavBar.vue (2 hunks)
  • web/src/components/SecurityAlert.vue (7 hunks)
  • web/src/components/ThemeToggle.vue (2 hunks)
  • web/src/components/common/ProxyKeysInput.vue (8 hunks)
  • web/src/components/keys/GroupCopyModal.vue (7 hunks)
  • web/src/components/keys/GroupFormModal.vue (27 hunks)
  • web/src/components/keys/GroupInfoCard.vue (21 hunks)
  • web/src/components/keys/GroupList.vue (4 hunks)
  • web/src/components/keys/KeyCreateDialog.vue (5 hunks)
  • web/src/components/keys/KeyDeleteDialog.vue (5 hunks)
  • web/src/components/keys/KeyTable.vue (30 hunks)
  • web/src/components/logs/LogTable.vue (22 hunks)
  • web/src/locales/en-US.ts (1 hunks)
  • web/src/locales/index.ts (1 hunks)
  • web/src/locales/ja-JP.ts (1 hunks)
  • web/src/locales/zh-CN.ts (1 hunks)
  • web/src/main.ts (1 hunks)
  • web/src/utils/http.ts (4 hunks)
  • web/src/views/Login.vue (5 hunks)
  • web/src/views/Settings.vue (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
internal/app/app.go (1)
internal/i18n/i18n.go (1)
  • Init (17-30)
internal/router/router.go (1)
internal/i18n/middleware.go (1)
  • Middleware (16-33)
internal/i18n/i18n.go (1)
internal/i18n/middleware.go (1)
  • Message (107-110)
internal/i18n/middleware.go (1)
internal/i18n/i18n.go (2)
  • GetLocalizer (47-57)
  • T (111-127)
🔇 Additional comments (20)
web/src/components/BaseInfoCard.vue (1)

6-9: LGTM: i18n wiring is correct

useI18n and t() usage align with the new locale keys.

web/src/locales/en-US.ts (1)

98-110: LGTM: dashboard keys cover new component usage

Keys totalKeys, rpm10Min, requests24h, errorRate24h exist and match component references.

go.mod (2)

5-5: Ensure CI runners support Go 1.24.x
The new toolchain go1.24.3 directive in go.mod (line 5) requires build agents to run Go 1.24.3 or later; verify your CI pipelines are updated accordingly.


16-22: Approve dependency additions
Verified go.mod contains github.com/nicksnyder/go-i18n/v2 v2.6.0 and golang.org/x/text v0.28.0, and go list -m all confirms those exact versions are in use. No unintended upgrades detected.

web/src/api/keys.ts (1)

1-1: Good: error message localized

Importing i18n and using auth.noAuthKeyFound aligns with the new i18n setup.

web/src/main.ts (1)

4-9: LGTM: i18n plugin registration

Plugin order is fine; app will have translations available globally.

web/src/api/logs.ts (1)

1-21: Good: export error localized

Consistent with keys.ts; no API surface change.

web/src/components/Logout.vue (1)

5-8: LGTM — i18n adoption is correct.

Label migrated to t("nav.logout") without altering behavior.

Also applies to: 23-23

internal/app/app.go (1)

13-13: Good: initialize i18n during startup and fail fast on errors.

This ordering is sensible; early failure prevents partial boot.

Verify locale assets availability in all deploy modes.

If internal/i18n loads files from disk, ensure they’re embedded (e.g., go:embed) or mounted in containers; otherwise Init may fail at runtime.

Would you like me to scan internal/i18n for go:embed usage and middleware wiring?

Also applies to: 79-84

web/src/components/Layout.vue (1)

4-4: LGTM — Language selector integrated in desktop and mobile.

Nice placement alongside theme/logout; no behavior changes.

Also applies to: 41-44, 57-57

web/src/components/keys/KeyCreateDialog.vue (1)

24-24: LGTM — consistent i18n keys and success toast.

Copy and modal strings are localized correctly.

Also applies to: 61-61, 73-74, 90-91, 97-100

web/src/components/keys/GroupList.vue (1)

7-11: LGTM — i18n adoption and UX copy look good.

Placeholders, empty states, and CTA align with keys.* namespace.

Also applies to: 82-87, 98-101, 137-138

web/src/views/Login.vue (1)

18-21: LGTM — comprehensive i18n migration on login view.

Validation, headings, and placeholders use proper keys; language selector placement is fine.

Also applies to: 45-47, 52-54, 62-62, 81-81, 92-98

web/src/components/AppFooter.vue (1)

144-148: LGTM — footer links tooltips and labels localized.

Copy and iconography are consistent; brand names left as-is (GitHub/Telegram), which is fine.

Also applies to: 162-163, 175-178, 189-193, 207-208

web/src/components/keys/KeyDeleteDialog.vue (1)

7-7: Good i18n migration; keys wired correctly across script and template.
All hard-coded strings are replaced with t(...), including dynamic group fallback. No functional regressions spotted.

Also applies to: 24-24, 62-62, 74-74, 91-91, 98-98, 100-100

web/src/components/GlobalTaskProgressBar.vue (1)

7-10: Solid i18n replacements for task titles and completion messages.
Keys and interpolations look consistent; button title localized too.

Also applies to: 52-52, 55-59, 62-65, 68-71, 131-142, 161-167

web/src/components/ThemeToggle.vue (1)

51-54: Template i18n usage looks correct.
Labels interpolate as expected.

web/src/utils/http.ts (1)

1-1: i18n in messages and errors looks good.
Translations are used consistently for success fallback and error branches.

Also applies to: 56-69

web/src/components/keys/GroupInfoCard.vue (2)

26-31: Good i18n wiring and usage of t().

Initialization with useI18n() and systematic replacement of literals looks solid.


26-31: All i18n keys covered. Automated verification confirms every key used in web/src/components/keys/GroupInfoCard.vue is present in en-US.ts, ja-JP.ts, and zh-CN.ts.

Comment on lines +16 to +30
// Init 初始化 i18n
func Init() error {
bundle = i18n.NewBundle(language.Chinese)
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)

// 加载支持的语言文件
languages := []string{"zh-CN", "en-US", "ja-JP"}
for _, lang := range languages {
if err := loadMessageFile(lang); err != nil {
return fmt.Errorf("failed to load language file %s: %w", lang, err)
}
}

return nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make Init idempotent and thread-safe to avoid duplicate loads and races

Guard initialization so multiple callers (or tests) don’t reload the bundle, and early calls from middleware won’t race.

Apply:

 func Init() error {
-	bundle = i18n.NewBundle(language.Chinese)
-	bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
-
-	// 加载支持的语言文件
-	languages := []string{"zh-CN", "en-US", "ja-JP"}
-	for _, lang := range languages {
-		if err := loadMessageFile(lang); err != nil {
-			return fmt.Errorf("failed to load language file %s: %w", lang, err)
-		}
-	}
-
-	return nil
+	var err error
+	initOnce.Do(func() {
+		bundle = i18n.NewBundle(language.Chinese)
+		bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
+		langs := []string{"zh-CN", "en-US", "ja-JP"}
+		for _, lang := range langs {
+			if e := loadMessageFile(lang); e != nil {
+				err = fmt.Errorf("failed to load language file %s: %w", lang, e)
+				return
+			}
+		}
+	})
+	return err
 }

Add once helpers:

 import (
 	"encoding/json"
 	"fmt"
 	"strings"
+	"sync"
 
 	"github.com/nicksnyder/go-i18n/v2/i18n"
 	"golang.org/x/text/language"
 )
 
 var (
 	bundle *i18n.Bundle
+	initOnce sync.Once
 )

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In internal/i18n/i18n.go around lines 16 to 30, Init is not idempotent or
thread-safe; make it safe by adding a package-level sync.Once (e.g. initOnce)
and a package-level error variable (e.g. initErr), then wrap the existing
initialization logic inside initOnce.Do(func(){ ... }) and set initErr inside
that closure if any step fails; finally have Init return initErr so concurrent
callers get the same result and initialization runs exactly once. Ensure bundle
remains a package-level variable initialized inside the once closure.

Comment on lines 46 to 57
// GetLocalizer 获取本地化器
func GetLocalizer(acceptLang string) *i18n.Localizer {
// 解析 Accept-Language 头
langs := parseAcceptLanguage(acceptLang)

// 如果没有指定语言,默认使用中文
if len(langs) == 0 {
langs = []string{"zh-CN"}
}

return i18n.NewLocalizer(bundle, langs...)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Defensive: avoid nil-deref if GetLocalizer is called before Init

Best-effort lazy init prevents panics in tests or mis-ordered starts.

 func GetLocalizer(acceptLang string) *i18n.Localizer {
+	// 确保已初始化
+	if bundle == nil {
+		_ = Init()
+	}
 	// 解析 Accept-Language 头
 	langs := parseAcceptLanguage(acceptLang)
 	
 	// 如果没有指定语言,默认使用中文
 	if len(langs) == 0 {
 		langs = []string{"zh-CN"}
 	}
 	
 	return i18n.NewLocalizer(bundle, langs...)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// GetLocalizer 获取本地化器
func GetLocalizer(acceptLang string) *i18n.Localizer {
// 解析 Accept-Language 头
langs := parseAcceptLanguage(acceptLang)
// 如果没有指定语言,默认使用中文
if len(langs) == 0 {
langs = []string{"zh-CN"}
}
return i18n.NewLocalizer(bundle, langs...)
}
// GetLocalizer 获取本地化器
func GetLocalizer(acceptLang string) *i18n.Localizer {
// 确保已初始化
if bundle == nil {
_ = Init()
}
// 解析 Accept-Language 头
langs := parseAcceptLanguage(acceptLang)
// 如果没有指定语言,默认使用中文
if len(langs) == 0 {
langs = []string{"zh-CN"}
}
return i18n.NewLocalizer(bundle, langs...)
}
🤖 Prompt for AI Agents
In internal/i18n/i18n.go around lines 46 to 57, GetLocalizer currently assumes
the package-level bundle is initialized and may nil-deref if called before Init;
add a defensive lazy-init: check if bundle == nil and if so initialize it (call
the existing Init function or create a minimal bundle with default messages)
before using it, then proceed to parse Accept-Language and return
i18n.NewLocalizer(bundle, langs...); ensure this lazy-init is safe for tests and
concurrent callers (use the existing init path or a sync.Once if there are
concurrency concerns).

Comment on lines 115 to 117
<div class="stat-value">
{{ stats?.rpm?.value.toFixed(1) ?? 0 }}
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix potential runtime error: toFixed on undefined

stats?.rpm?.value.toFixed(1) throws when value is undefined. Guard before calling.

Apply:

-                {{ stats?.rpm?.value.toFixed(1) ?? 0 }}
+                {{ (stats?.rpm?.value ?? 0).toFixed(1) }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="stat-value">
{{ stats?.rpm?.value.toFixed(1) ?? 0 }}
</div>
<div class="stat-value">
{{ (stats?.rpm?.value ?? 0).toFixed(1) }}
</div>
🤖 Prompt for AI Agents
In web/src/components/BaseInfoCard.vue around lines 115-117, calling
stats?.rpm?.value.toFixed(1) can throw if value is undefined; guard before
invoking toFixed by first retrieving the value (e.g. const rpmValue =
stats?.rpm?.value) and render a safe fallback when it's not a number — for
example, use a conditional that checks typeof rpmValue === 'number' and calls
rpmValue.toFixed(1), otherwise render 0 (or another chosen fallback).

Comment on lines 58 to 62
const statusOptions = [
{ label: "全部", value: "all" },
{ label: "有效", value: "active" },
{ label: "无效", value: "invalid" },
{ label: t("common.all"), value: "all" },
{ label: t("keys.valid"), value: "active" },
{ label: t("keys.invalid"), value: "invalid" },
];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix non-reactive translations in statusOptions

Options are created once; they won’t update when the locale changes. Use computed.

-const statusOptions = [
-  { label: t("common.all"), value: "all" },
-  { label: t("keys.valid"), value: "active" },
-  { label: t("keys.invalid"), value: "invalid" },
-];
+const statusOptions = computed(() => [
+  { label: t("common.all"), value: "all" },
+  { label: t("keys.valid"), value: "active" },
+  { label: t("keys.invalid"), value: "invalid" },
+]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const statusOptions = [
{ label: "全部", value: "all" },
{ label: "有效", value: "active" },
{ label: "无效", value: "invalid" },
{ label: t("common.all"), value: "all" },
{ label: t("keys.valid"), value: "active" },
{ label: t("keys.invalid"), value: "invalid" },
];
const statusOptions = computed(() => [
{ label: t("common.all"), value: "all" },
{ label: t("keys.valid"), value: "active" },
{ label: t("keys.invalid"), value: "invalid" },
]);
🤖 Prompt for AI Agents
In web/src/components/keys/KeyTable.vue around lines 58 to 62, the statusOptions
array is created once using t(...) so it won’t reactively update when the locale
changes; replace the constant with a computed property (use Vue's computed) that
returns the array and call t(...) inside that computed, and ensure computed is
imported from 'vue' (and that t is obtained from useI18n or the current i18n
context) so the options update when the locale changes.

Comment on lines 64 to 85
// 更多操作下拉菜单选项
const moreOptions = [
{ label: "导出所有密钥", key: "copyAll" },
{ label: "导出有效密钥", key: "copyValid" },
{ label: "导出无效密钥", key: "copyInvalid" },
{ label: t("keys.exportAllKeys"), key: "copyAll" },
{ label: t("keys.exportValidKeys"), key: "copyValid" },
{ label: t("keys.exportInvalidKeys"), key: "copyInvalid" },
{ type: "divider" },
{ label: "恢复所有无效密钥", key: "restoreAll" },
{ label: "清空所有无效密钥", key: "clearInvalid", props: { style: { color: "#d03050" } } },
{ label: t("keys.restoreAllInvalidKeys"), key: "restoreAll" },
{
label: t("keys.clearAllInvalidKeys"),
key: "clearInvalid",
props: { style: { color: "#d03050" } },
},
{
label: "清空所有密钥",
label: t("keys.clearAllKeys"),
key: "clearAll",
props: { style: { color: "red", fontWeight: "bold" } },
},
{ type: "divider" },
{ label: "验证所有密钥", key: "validateAll" },
{ label: "验证有效密钥", key: "validateActive" },
{ label: "验证无效密钥", key: "validateInvalid" },
{ label: t("keys.validateAllKeys"), key: "validateAll" },
{ label: t("keys.validateValidKeys"), key: "validateActive" },
{ label: t("keys.validateInvalidKeys"), key: "validateInvalid" },
];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Same issue for moreOptions: make it computed so labels react to locale changes

Currently frozen at creation time.

-const moreOptions = [
+const moreOptions = computed(() => [
   { label: t("keys.exportAllKeys"), key: "copyAll" },
   { label: t("keys.exportValidKeys"), key: "copyValid" },
   { label: t("keys.exportInvalidKeys"), key: "copyInvalid" },
   { type: "divider" },
   { label: t("keys.restoreAllInvalidKeys"), key: "restoreAll" },
   {
     label: t("keys.clearAllInvalidKeys"),
     key: "clearInvalid",
     props: { style: { color: "#d03050" } },
   },
   {
     label: t("keys.clearAllKeys"),
     key: "clearAll",
     props: { style: { color: "red", fontWeight: "bold" } },
   },
   { type: "divider" },
   { label: t("keys.validateAllKeys"), key: "validateAll" },
   { label: t("keys.validateValidKeys"), key: "validateActive" },
   { label: t("keys.validateInvalidKeys"), key: "validateInvalid" },
-];
+]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 更多操作下拉菜单选项
const moreOptions = [
{ label: "导出所有密钥", key: "copyAll" },
{ label: "导出有效密钥", key: "copyValid" },
{ label: "导出无效密钥", key: "copyInvalid" },
{ label: t("keys.exportAllKeys"), key: "copyAll" },
{ label: t("keys.exportValidKeys"), key: "copyValid" },
{ label: t("keys.exportInvalidKeys"), key: "copyInvalid" },
{ type: "divider" },
{ label: "恢复所有无效密钥", key: "restoreAll" },
{ label: "清空所有无效密钥", key: "clearInvalid", props: { style: { color: "#d03050" } } },
{ label: t("keys.restoreAllInvalidKeys"), key: "restoreAll" },
{
label: t("keys.clearAllInvalidKeys"),
key: "clearInvalid",
props: { style: { color: "#d03050" } },
},
{
label: "清空所有密钥",
label: t("keys.clearAllKeys"),
key: "clearAll",
props: { style: { color: "red", fontWeight: "bold" } },
},
{ type: "divider" },
{ label: "验证所有密钥", key: "validateAll" },
{ label: "验证有效密钥", key: "validateActive" },
{ label: "验证无效密钥", key: "validateInvalid" },
{ label: t("keys.validateAllKeys"), key: "validateAll" },
{ label: t("keys.validateValidKeys"), key: "validateActive" },
{ label: t("keys.validateInvalidKeys"), key: "validateInvalid" },
];
// 更多操作下拉菜单选项
const moreOptions = computed(() => [
{ label: t("keys.exportAllKeys"), key: "copyAll" },
{ label: t("keys.exportValidKeys"), key: "copyValid" },
{ label: t("keys.exportInvalidKeys"), key: "copyInvalid" },
{ type: "divider" },
{ label: t("keys.restoreAllInvalidKeys"), key: "restoreAll" },
{
label: t("keys.clearAllInvalidKeys"),
key: "clearInvalid",
props: { style: { color: "#d03050" } },
},
{
label: t("keys.clearAllKeys"),
key: "clearAll",
props: { style: { color: "red", fontWeight: "bold" } },
},
{ type: "divider" },
{ label: t("keys.validateAllKeys"), key: "validateAll" },
{ label: t("keys.validateValidKeys"), key: "validateActive" },
{ label: t("keys.validateInvalidKeys"), key: "validateInvalid" },
]);
🤖 Prompt for AI Agents
In web/src/components/keys/KeyTable.vue around lines 64 to 85, the moreOptions
array is created once using t(...) so its labels are frozen and won't update
when the locale changes; change moreOptions to a computed property (e.g., const
moreOptions = computed(() => [ ... ]) ) so it re-evaluates t(...) on locale
change, import/combine Vue's computed if not already imported, and update any
usages to reference moreOptions.value.

Comment on lines 67 to 75
const successOptions = [
{ label: "成功", value: "true" },
{ label: "失败", value: "false" },
{ label: t("common.success"), value: "true" },
{ label: t("common.error"), value: "false" },
];

const requestTypeOptions = [
{ label: "重试请求", value: "retry" },
{ label: "最终请求", value: "final" },
{ label: t("logs.retryRequest"), value: "retry" },
{ label: t("logs.finalRequest"), value: "final" },
];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make option labels reactive to locale changes

Static arrays won’t update when the user switches language.

-const successOptions = [
-  { label: t("common.success"), value: "true" },
-  { label: t("common.error"), value: "false" },
-];
-
-const requestTypeOptions = [
-  { label: t("logs.retryRequest"), value: "retry" },
-  { label: t("logs.finalRequest"), value: "final" },
-];
+const successOptions = computed(() => [
+  { label: t("common.success"), value: "true" },
+  { label: t("common.error"), value: "false" },
+]);
+
+const requestTypeOptions = computed(() => [
+  { label: t("logs.retryRequest"), value: "retry" },
+  { label: t("logs.finalRequest"), value: "final" },
+]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const successOptions = [
{ label: "成功", value: "true" },
{ label: "失败", value: "false" },
{ label: t("common.success"), value: "true" },
{ label: t("common.error"), value: "false" },
];
const requestTypeOptions = [
{ label: "重试请求", value: "retry" },
{ label: "最终请求", value: "final" },
{ label: t("logs.retryRequest"), value: "retry" },
{ label: t("logs.finalRequest"), value: "final" },
];
const successOptions = computed(() => [
{ label: t("common.success"), value: "true" },
{ label: t("common.error"), value: "false" },
]);
const requestTypeOptions = computed(() => [
{ label: t("logs.retryRequest"), value: "retry" },
{ label: t("logs.finalRequest"), value: "final" },
]);
🤖 Prompt for AI Agents
In web/src/components/logs/LogTable.vue around lines 67 to 75, the option arrays
are created once with t(...) so labels won't update when locale changes; replace
the static arrays with reactive computed getters (e.g., const successOptions =
computed(() => [{ label: t("common.success"), value: "true" }, { label:
t("common.error"), value: "false" }]) and similarly for requestTypeOptions) so
t(...) is invoked on each access and picks up locale changes, and import
computed from 'vue' / define them inside setup (or return functions that build
the arrays) so the UI updates when the locale changes.

Comment on lines +166 to +169
onePerLine: "One per line",
separated: "Separator separated",
keyPlaceholder: "Enter keys, one per line",
noGroups: "No groups",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Polish wording: “Separator separated” → “Delimiter separated”

User-facing text quality.

Apply:

-    separated: "Separator separated",
+    separated: "Delimiter separated",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onePerLine: "One per line",
separated: "Separator separated",
keyPlaceholder: "Enter keys, one per line",
noGroups: "No groups",
onePerLine: "One per line",
separated: "Delimiter separated",
keyPlaceholder: "Enter keys, one per line",
noGroups: "No groups",
🤖 Prompt for AI Agents
In web/src/locales/en-US.ts around lines 166 to 169, the user-facing string
"Separator separated" is awkward; update it to "Delimiter separated" to improve
wording consistency and clarity for users. Replace the value of the appropriate
key (currently set to "Separator separated") with "Delimiter separated" and
ensure spacing/casing matches other locale entries.

Comment on lines +16 to +41
// 获取默认语言
function getDefaultLocale(): Locale {
// 1. 优先使用 localStorage 中保存的语言
const savedLocale = localStorage.getItem("locale");
if (savedLocale && SUPPORTED_LOCALES.some(l => l.key === savedLocale)) {
return savedLocale as Locale;
}

// 2. 自动检测浏览器语言
const browserLang = navigator.language;

// 精确匹配
if (SUPPORTED_LOCALES.some(l => l.key === browserLang)) {
return browserLang as Locale;
}

// 模糊匹配(如 zh 匹配 zh-CN)
const shortLang = browserLang.split("-")[0];
const matched = SUPPORTED_LOCALES.find(l => l.key.startsWith(shortLang));
if (matched) {
return matched.key;
}

// 3. 默认中文
return "zh-CN";
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden default-locale resolution for SSR/private mode.

Accessing localStorage/navigator at module init can throw. Guard and fall back.

-function getDefaultLocale(): Locale {
-  // 1. 优先使用 localStorage 中保存的语言
-  const savedLocale = localStorage.getItem("locale");
-  if (savedLocale && SUPPORTED_LOCALES.some(l => l.key === savedLocale)) {
-    return savedLocale as Locale;
-  }
-
-  // 2. 自动检测浏览器语言
-  const browserLang = navigator.language;
-
-  // 精确匹配
-  if (SUPPORTED_LOCALES.some(l => l.key === browserLang)) {
-    return browserLang as Locale;
-  }
-
-  // 模糊匹配(如 zh 匹配 zh-CN)
-  const shortLang = browserLang.split("-")[0];
-  const matched = SUPPORTED_LOCALES.find(l => l.key.startsWith(shortLang));
-  if (matched) {
-    return matched.key;
-  }
-
-  // 3. 默认中文
-  return "zh-CN";
-}
+function getDefaultLocale(): Locale {
+  try {
+    const savedLocale = typeof window !== "undefined" ? localStorage.getItem("locale") : null;
+    if (savedLocale && SUPPORTED_LOCALES.some(l => l.key === savedLocale)) {
+      return savedLocale as Locale;
+    }
+    const browserLang =
+      typeof navigator !== "undefined" && navigator.language ? navigator.language : "";
+    if (SUPPORTED_LOCALES.some(l => l.key === browserLang)) {
+      return browserLang as Locale;
+    }
+    const shortLang = browserLang.split("-")[0];
+    const matched = SUPPORTED_LOCALES.find(l => l.key.startsWith(shortLang));
+    if (matched) return matched.key;
+  } catch { /* noop */ }
+  return "zh-CN";
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 获取默认语言
function getDefaultLocale(): Locale {
// 1. 优先使用 localStorage 中保存的语言
const savedLocale = localStorage.getItem("locale");
if (savedLocale && SUPPORTED_LOCALES.some(l => l.key === savedLocale)) {
return savedLocale as Locale;
}
// 2. 自动检测浏览器语言
const browserLang = navigator.language;
// 精确匹配
if (SUPPORTED_LOCALES.some(l => l.key === browserLang)) {
return browserLang as Locale;
}
// 模糊匹配(如 zh 匹配 zh-CN)
const shortLang = browserLang.split("-")[0];
const matched = SUPPORTED_LOCALES.find(l => l.key.startsWith(shortLang));
if (matched) {
return matched.key;
}
// 3. 默认中文
return "zh-CN";
}
function getDefaultLocale(): Locale {
try {
const savedLocale = typeof window !== "undefined" ? localStorage.getItem("locale") : null;
if (savedLocale && SUPPORTED_LOCALES.some(l => l.key === savedLocale)) {
return savedLocale as Locale;
}
const browserLang =
typeof navigator !== "undefined" && navigator.language ? navigator.language : "";
if (SUPPORTED_LOCALES.some(l => l.key === browserLang)) {
return browserLang as Locale;
}
const shortLang = browserLang.split("-")[0];
const matched = SUPPORTED_LOCALES.find(l => l.key.startsWith(shortLang));
if (matched) return matched.key;
} catch {
/* noop */
}
return "zh-CN";
}
🤖 Prompt for AI Agents
In web/src/locales/index.ts around lines 16 to 41, the default locale resolution
currently accesses localStorage and navigator directly which can throw in SSR or
private mode; wrap those accesses in environment checks and try/catch blocks:
first ensure window is defined before using localStorage or navigator, attempt
to read localStorage.getItem and navigator.language inside try/catch (falling
back silently on errors), and if any access fails or yields no match continue to
browser-independent checks and finally return the default "zh-CN"; keep the same
matching logic but make all storage and navigator reads defensive to avoid
exceptions during module initialization.

Comment on lines +550 to +551
"キー検証完了、{total}個のキーを処理、{valid}個成功、{invalid}個失敗。注意:検証失敗でもすぐにブラックリストに追加されるわけではありません。失敗回数が闾値に達する必要があります。",
importCompleted: "キーインポート完了、{added}個追加、{ignored}個無視。",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Typo: “闾値” should be “閾値”

This is a visible user-facing typo.

-      "キー検証完了、{total}個のキーを処理、{valid}個成功、{invalid}個失敗。注意:検証失敗でもすぐにブラックリストに追加されるわけではありません。失敗回数が闾値に達する必要があります。",
+      "キー検証完了、{total}個のキーを処理、{valid}個成功、{invalid}個失敗。注意:検証失敗でもすぐにブラックリストに追加されるわけではありません。失敗回数が閾値に達する必要があります。",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"キー検証完了、{total}個のキーを処理、{valid}個成功、{invalid}個失敗。注意:検証失敗でもすぐにブラックリストに追加されるわけではありません。失敗回数が闾値に達する必要があります。",
importCompleted: "キーインポート完了、{added}個追加、{ignored}個無視。",
"キー検証完了、{total}個のキーを処理、{valid}個成功、{invalid}個失敗。注意:検証失敗でもすぐにブラックリストに追加されるわけではありません。失敗回数が閾値に達する必要があります。",
importCompleted: "キーインポート完了、{added}個追加、{ignored}個無視。",
🤖 Prompt for AI Agents
In web/src/locales/ja-JP.ts around lines 550 to 551, the string contains a typo:
the kanji "闾値" should be corrected to "閾値"; update the localized message to use
"閾値" so the sentence reads "失敗回数が閾値に達する必要があります。" and ensure no other occurrences
of "闾値" remain in the file.

@tbphp tbphp self-assigned this Sep 7, 2025
@tbphp tbphp added the enhancement New feature or request label Sep 7, 2025
@tbphp tbphp modified the milestone: v1.1.0 Sep 7, 2025
@tbphp tbphp linked an issue Sep 7, 2025 that may be closed by this pull request
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
web/src/components/keys/GroupInfoCard.vue (1)

53-54: Fix NCollapse accordion model type

With accordion enabled, v-model:expanded-names must be string | number | null, but expandedName is string[]. This can cause runtime/type issues.

Apply:

-const expandedName = ref<string[]>([]);
+const expandedName = ref<string | null>(null);

And reset:

 function resetPage() {
   showEditModal.value = false;
   showCopyModal.value = false;
-  expandedName.value = [];
+  expandedName.value = null;
 }

Also applies to: 450-452, 283-286

internal/handler/settings_handler.go (1)

86-90: Localize the success message returned by UpdateSettings.

Hard-coded English string breaks i18n. Use existing key config.updated.

Apply:

-	response.Success(c, gin.H{
-		"message": "Settings updated successfully. Configuration will be reloaded in the background across all instances.",
-	})
+	response.Success(c, gin.H{
+		"message": i18n.Message(c, "config.updated"),
+	})

If you want to keep the reload notice, add a new key (e.g., config.reload_notice) across locales and include it as a separate field.

♻️ Duplicate comments (2)
internal/i18n/i18n.go (2)

18-32: Make Init idempotent and thread-safe.

Guard against duplicate loads/races; return the first error.

Apply:

@@
-import (
+import (
   "encoding/json"
   "fmt"
   "strings"
+  "sync"
@@
   "golang.org/x/text/language"
 )
 
 var (
-  bundle *i18n.Bundle
+  bundle   *i18n.Bundle
+  initOnce sync.Once
+  initErr  error
 )
 
 // Init 初始化 i18n
 func Init() error {
-  bundle = i18n.NewBundle(language.Chinese)
-  bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
-
-  // 加载支持的语言文件
-  languages := []string{"zh-CN", "en-US", "ja-JP"}
-  for _, lang := range languages {
-    if err := loadMessageFile(lang); err != nil {
-      return fmt.Errorf("failed to load language file %s: %w", lang, err)
-    }
-  }
-
-  return nil
+  initOnce.Do(func() {
+    b := i18n.NewBundle(language.Chinese)
+    b.RegisterUnmarshalFunc("json", json.Unmarshal)
+    for _, lang := range []string{"zh-CN", "en-US", "ja-JP"} {
+      if err := loadMessageFileWith(b, lang); err != nil {
+        initErr = fmt.Errorf("failed to load language file %s: %w", lang, err)
+        return
+      }
+    }
+    bundle = b
+  })
+  return initErr
 }
 
-// loadMessageFile 加载语言文件
-func loadMessageFile(lang string) error {
+// loadMessageFile 加载语言文件
+func loadMessageFile(lang string) error { return loadMessageFileWith(bundle, lang) }
+
+func loadMessageFileWith(b *i18n.Bundle, lang string) error {
   // 根据语言设置消息
   messages := getMessages(lang)
   for id, msg := range messages {
-    bundle.AddMessages(language.MustParse(lang), &i18n.Message{
+    b.AddMessages(language.MustParse(lang), &i18n.Message{
       ID:    id,
       Other: msg,
     })
   }
 
   return nil
 }

48-59: Defensive lazy init in GetLocalizer.

Avoid nil-deref if called before Init.

Apply:

 func GetLocalizer(acceptLang string) *i18n.Localizer {
+  if bundle == nil {
+    _ = Init()
+  }
   // 解析 Accept-Language 头
   langs := parseAcceptLanguage(acceptLang)
@@
   return i18n.NewLocalizer(bundle, langs...)
 }
🧹 Nitpick comments (17)
web/src/components/keys/GroupInfoCard.vue (7)

156-163: Surface a localized error when loading config options fails

Right now failures only log to console; users get no feedback.

Apply:

 async function loadConfigOptions() {
   try {
     const options = await keysApi.getGroupConfigOptions();
     configOptions.value = options || [];
   } catch (error) {
-    console.error("Failed to load config options:", error);
+    console.error("[GroupInfoCard] loadConfigOptions failed:", error);
+    window.$message?.error(t("keys.loadConfigOptionsFailed"));
   }
 }

I can add the missing i18n key if needed.


203-207: Make the delete confirmation fully translatable (sentence order + emphasis)

The sentence is assembled from multiple t() calls around a hardcoded strong block, which won’t localize well in languages with different word order.

Prefer a single i18n string with interpolation and markup. Example:

- content: () =>
-   h("div", null, [
-     h("p", null, [
-       t("keys.dangerousOperation"),
-       h("strong", { style: { color: "#d03050" } }, props.group?.name),
-       t("keys.toConfirmDeletion"),
-     ]),
-     ...
-   ]),
+ content: () =>
+   h("div", null, [
+     h("p", null, [
+       h("span", { innerHTML: t("keys.confirmDeleteParagraph", { name: `<strong style='color:#d03050'>${props.group?.name}</strong>` }) })
+     ]),
+     ...
+   ]),

Add keys.confirmDeleteParagraph like: “This is dangerous. Enter {name} to confirm deletion.” allowing translators to place {name} anywhere. Also consider a single dialog (with input) to reduce steps.

Also applies to: 210-217, 224-228, 231-231, 239-244


354-355: Avoid hardcoded Chinese colon “:”; move punctuation into translations

Labels embed a fullwidth colon, which looks wrong in non-CJK locales. Let translators control punctuation.

For example:

-:label="`${t('keys.keyCount')}:${stats?.key_stats?.total_keys ?? 0}`"
+:label="t('keys.keyCountWithTotal', { total: formatNumber(stats?.key_stats?.total_keys ?? 0) })"

Repeat for other labels (groupName, displayName, channelType, sortOrder, testModel, testPath, proxyKeys, description, customHeaders, paramOverrides). I can batch-generate the diffs and placeholder keys.

Also applies to: 376-377, 399-400, 422-423, 459-459, 464-464, 469-469, 474-474, 479-479, 484-484, 489-489, 519-519, 579-579, 600-601


352-444: Localize number/percent formatting (compact notation, percent symbol, digits)

“K” suffix and manual “%” are not locale-aware.

Use Intl.NumberFormat with the current locale:

- function formatNumber(num: number): string {
-   if (num >= 1000) {
-     return `${(num / 1000).toFixed(1)}K`;
-   }
-   return num.toString();
- }
-
- function formatPercentage(num: number): string {
-   if (num <= 0) {
-     return "0";
-   }
-   return `${(num * 100).toFixed(1)}%`;
- }
+ const { locale } = useI18n();
+ function formatNumber(num: number | undefined): string {
+   const n = num ?? 0;
+   return new Intl.NumberFormat(locale.value, {
+     notation: "compact",
+     maximumFractionDigits: 1,
+   }).format(n);
+ }
+ function formatPercentage(num: number | undefined): string {
+   const n = Math.max(0, num ?? 0);
+   return new Intl.NumberFormat(locale.value, {
+     style: "percent",
+     maximumFractionDigits: 1,
+   }).format(n);
+ }

This will correctly render for en/zh/ja.

Also applies to: 253-268


531-542: Translate “Upstream N:” and “Weight” together, not piecemeal

Concatenation with index and ASCII “:” won’t localize well.

Apply:

-:label="`${t('keys.upstream')} ${index + 1}:`"
+:label="t('keys.upstreamIndex', { index: index + 1 })"
...
-<n-tag size="small" type="info">
-  {{ t("keys.weight") }}: {{ upstream.weight }}
-</n-tag>
+<n-tag size="small" type="info">
+  {{ t("keys.weightLabel', { weight: upstream.weight }) }}
+ </n-tag>

Add keys.upstreamIndex like “Upstream {index}” and keys.weightLabel like “Weight: {weight}”.

Also applies to: 536-537


592-595: Use a dedicated “Removed”/“Deleted” label

Rendering common.delete here reads as an action, not a state. Prefer a noun/adjective (“Removed”).

-<span class="header-removed" v-else>{{ t("common.delete") }}</span>
+<span class="header-removed" v-else>{{ t("keys.headerRemoved") }}</span>

88-91: Optional: guard against stale stats overwrites

Multiple rapid loadStats() calls (prop change + triggers) may race and overwrite newer data.

Add a simple request token:

 let statsReqId = 0;
 async function loadStats() {
   if (!props.group?.id) {
     stats.value = null;
     return;
   }
-  try {
+  const reqId = ++statsReqId;
+  try {
     loading.value = true;
     if (props.group?.id) {
-      stats.value = await keysApi.getGroupStats(props.group.id);
+      const res = await keysApi.getGroupStats(props.group.id);
+      if (reqId === statsReqId) stats.value = res;
     }
   } finally {
     loading.value = false;
   }
 }

Also applies to: 93-99, 101-121, 123-138, 146-154

internal/i18n/locales/ja-JP.go (1)

90-97: Placeholder consistency check (nit).

Placeholders like {keyType}, {length}, {pattern} should match data passed at call sites; otherwise users will see raw braces.

If your i18n.Message supports defaults/templatization with named args, consider adding unit tests that render these with sample data.

internal/types/types.go (1)

22-43: Guard against typos in tag keys (optional).

Using raw string keys in tags can drift. Consider centralizing config.* keys as typed consts and referencing them via code generation for tags, or add a linter to validate tags map to existing locale entries.

#!/bin/bash
# Quick check: list all tag values that look like i18n keys from SystemSettings
rg -nP 'name:"config\.[^"]+"|category:"config\.[^"]+"|desc:"config\.[^"]+"' internal/types/types.go
internal/handler/settings_handler.go (2)

21-35: Deduplicate translation logic with a tiny helper.

Shrinks repetition and keeps the “config.” check in one place.

Apply:

-	// Translate settings info
-	for i := range settingsInfo {
-		// Translate name if it's an i18n key
-		if strings.HasPrefix(settingsInfo[i].Name, "config.") {
-			settingsInfo[i].Name = i18n.Message(c, settingsInfo[i].Name)
-		}
-		// Translate description if it's an i18n key
-		if strings.HasPrefix(settingsInfo[i].Description, "config.") {
-			settingsInfo[i].Description = i18n.Message(c, settingsInfo[i].Description)
-		}
-		// Translate category if it's an i18n key
-		if strings.HasPrefix(settingsInfo[i].Category, "config.") {
-			settingsInfo[i].Category = i18n.Message(c, settingsInfo[i].Category)
-		}
-	}
+	// Translate settings info
+	translate := func(v string) string {
+		if strings.HasPrefix(v, "config.") {
+			return i18n.Message(c, v)
+		}
+		return v
+	}
+	for i := range settingsInfo {
+		settingsInfo[i].Name = translate(settingsInfo[i].Name)
+		settingsInfo[i].Description = translate(settingsInfo[i].Description)
+		settingsInfo[i].Category = translate(settingsInfo[i].Category)
+	}

86-86: Avoid arbitrary sleep for consistency.

The fixed 100ms wait may be either insufficient or unnecessary. Prefer returning immediately with a “reload in background” notice, or await a concrete signal if strict consistency is required.

internal/handler/key_handler.go (2)

367-367: Localize success messages for consistency.

Return localized strings for "restored"/"cleared" counts so UI is fully internationalized.

Apply:

--- a/internal/handler/key_handler.go
+++ b/internal/handler/key_handler.go
@@
 import (
   "fmt"
   app_errors "gpt-load/internal/errors"
   "gpt-load/internal/models"
   "gpt-load/internal/response"
+  "gpt-load/internal/i18n"
   "log"
   "strconv"
   "strings"
   "time"
@@
-  response.Success(c, gin.H{"message": fmt.Sprintf("%d keys restored.", rowsAffected)})
+  response.Success(c, gin.H{
+    "message": i18n.Message(c, "success.keys_restored", map[string]interface{}{"count": rowsAffected}),
+  })
@@
-  response.Success(c, gin.H{"message": fmt.Sprintf("%d invalid keys cleared.", rowsAffected)})
+  response.Success(c, gin.H{
+    "message": i18n.Message(c, "success.invalid_keys_cleared", map[string]interface{}{"count": rowsAffected}),
+  })
@@
-  response.Success(c, gin.H{"message": fmt.Sprintf("%d keys cleared.", rowsAffected)})
+  response.Success(c, gin.H{
+    "message": i18n.Message(c, "success.all_keys_cleared", map[string]interface{}{"count": rowsAffected}),
+  })

Optional: also localize the fallback string "failed-to-decrypt" (Line 170) by adding a locale key (e.g., key.decryption_failed) and using i18n.Message.

Also applies to: 388-388, 409-409


134-138: Consider i18n for query param errors.

Errors from validateGroupIDFromQuery bubble to clients as English strings. Consider mapping to "validation.invalid_group_id" via ErrorI18nFromAPIError for consistency.

internal/handler/group_handler.go (3)

209-214: Localize config validation errors; log internals.

Current responses leak raw error text. Return a generic localized validation error and log details.

Apply:

@@
-  cleanedConfig, err := s.validateAndCleanConfig(req.Config)
-  if err != nil {
-    response.Error(c, app_errors.NewAPIError(app_errors.ErrValidation, fmt.Sprintf("Invalid config format: %v", err)))
-    return
-  }
+  cleanedConfig, err := s.validateAndCleanConfig(req.Config)
+  if err != nil {
+    logrus.WithError(err).Warn("invalid group config overrides")
+    response.ErrorI18nFromAPIError(c, app_errors.ErrValidation, "invalid_param")
+    return
+  }
@@
-    cleanedConfig, err := s.validateAndCleanConfig(req.Config)
-    if err != nil {
-      response.Error(c, app_errors.NewAPIError(app_errors.ErrValidation, fmt.Sprintf("Invalid config format: %w", err)))
-      return
-    }
+    cleanedConfig, err := s.validateAndCleanConfig(req.Config)
+    if err != nil {
+      logrus.WithError(err).Warn("invalid group config overrides")
+      response.ErrorI18nFromAPIError(c, app_errors.ErrValidation, "invalid_param")
+      return
+    }

Also applies to: 410-416


626-626: Localize delete success message.

Return "success.group_deleted" to align with the rest of i18n.

Apply:

- response.Success(c, gin.H{"message": "Group and associated keys deleted successfully"})
+ response.Success(c, gin.H{
+   "message": i18n.Message(c, "success.group_deleted"),
+ })

46-80: Optional: i18n for upstream validation errors.

These fmt.Errorf messages are user-facing; consider mapping them to existing "error.*" keys in locales or returning typed errors that handlers can localize.

internal/handler/dashboard_handler.go (1)

23-24: Localize database error responses.

Use ErrorI18nFromAPIError with existing keys.

Apply:

- response.Error(c, app_errors.NewAPIError(app_errors.ErrDatabase, "failed to get rpm stats"))
+ response.ErrorI18nFromAPIError(c, app_errors.ErrDatabase, "database.rpm_stats_failed")
@@
- response.Error(c, app_errors.NewAPIError(app_errors.ErrDatabase, "failed to get current period stats"))
+ response.ErrorI18nFromAPIError(c, app_errors.ErrDatabase, "database.current_stats_failed")
@@
- response.Error(c, app_errors.NewAPIError(app_errors.ErrDatabase, "failed to get previous period stats"))
+ response.ErrorI18nFromAPIError(c, app_errors.ErrDatabase, "database.previous_stats_failed")
@@
- response.Error(c, app_errors.NewAPIError(app_errors.ErrDatabase, "failed to get chart data"))
+ response.ErrorI18nFromAPIError(c, app_errors.ErrDatabase, "database.chart_data_failed")

Also applies to: 31-32, 36-37, 128-129

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 366043f and a540d5e.

📒 Files selected for processing (12)
  • internal/handler/dashboard_handler.go (5 hunks)
  • internal/handler/group_handler.go (17 hunks)
  • internal/handler/key_handler.go (5 hunks)
  • internal/handler/settings_handler.go (2 hunks)
  • internal/handler/task_handler.go (1 hunks)
  • internal/i18n/i18n.go (1 hunks)
  • internal/i18n/locales/en-US.go (1 hunks)
  • internal/i18n/locales/ja-JP.go (1 hunks)
  • internal/i18n/locales/zh-CN.go (1 hunks)
  • internal/response/response.go (2 hunks)
  • internal/types/types.go (1 hunks)
  • web/src/components/keys/GroupInfoCard.vue (21 hunks)
🔇 Additional comments (26)
web/src/components/keys/GroupInfoCard.vue (8)

26-26: Good i18n wiring

Using vue-i18n’s t() across UI strings is consistent and correct.

Also applies to: 30-31


170-173: Nice fallback copy

Falling back to a localized “no description” keeps the UI coherent.


276-279: LGTM on copy URL messaging

Good reuse of i18n for success/failure toasts.


321-326: Button tooltips/titles localized correctly

Edit/Delete/Copy titles wired to t(); looks good.

Also applies to: 336-337


82-85: LGTM on localized copy feedback

Good success/error messaging via t().


451-451: Good: section title localized

Detail section header now uses t(); consistent with the i18n effort.


296-304: Header i18n and copy-to-clipboard UX look good

SelectGroup fallback and tooltip copy hint are localized correctly.


1-290: Verification: ensure all i18n keys exist across locales

Automate a quick check that every t('...') key in this component exists in all locale JSONs.

#!/bin/bash
set -euo pipefail

COMP_FILE="web/src/components/keys/GroupInfoCard.vue"
TMP_KEYS="$(mktemp)"
rg -No --pcre2 "t\\(\\s*['\"]([^'\"]+)['\"]" "$COMP_FILE" | awk -F\" '{print $2}' | sort -u > "$TMP_KEYS"

python - << 'PY'
import json, glob, sys, os
keys = [k.strip() for k in open(os.environ["TMP_KEYS"]).read().splitlines() if k.strip()]
locale_files = glob.glob("web/src/locales/**/*.json", recursive=True)
if not locale_files:
    print("No locale JSON files found under web/src/locales", file=sys.stderr)
    sys.exit(2)

def has_path(obj, path):
    cur = obj
    for seg in path.split('.'):
        if not isinstance(cur, dict) or seg not in cur:
            return False
        cur = cur[seg]
    return True

missing = {}
for lf in locale_files:
    with open(lf, 'r', encoding='utf-8') as f:
        try:
            data = json.load(f)
        except Exception as e:
            print(f"Failed to parse {lf}: {e}", file=sys.stderr)
            continue
    miss = [k for k in keys if not has_path(data, k)]
    if miss:
        missing[lf] = miss

if missing:
    for lf, miss in missing.items():
        print(f"Missing in {lf}:")
        for k in miss:
            print(f"  - {k}")
    sys.exit(1)
else:
    print("All i18n keys in GroupInfoCard.vue are present in all locale files.")
PY
internal/i18n/locales/ja-JP.go (2)

4-154: Translations map looks consistent and complete.

Keys, placeholders, and categorization align with backend config.* usage. No functional issues spotted.


4-154: Verify bundle registration and key parity across locales.

Ensure:

  • MessagesJaJP is loaded into the i18n bundle with locale "ja-JP".
  • All keys here exist in zh-CN and en-US to avoid fallback-to-key at runtime.

Run to diff keys across backend locales:

#!/bin/bash
# Extract keys from Go locale maps (backend)
rg -nP --glob 'internal/i18n/locales/*.go' '^\s*"([a-z0-9._-]+)"\s*:' -o -r '$1' | awk -F: '{print $1,$2}' | sort -k1,1 -k2,2 > /tmp/backend-locale-keys.txt
echo "Sample (first 50):"
head -50 /tmp/backend-locale-keys.txt
# Group by key to find missing per locale
awk '{print $2}' /tmp/backend-locale-keys.txt | sort | uniq -c | awk '$1<3{print "Missing in some locale:",$0}'
internal/types/types.go (1)

22-43: Struct tag switch to i18n keys is correct and non-breaking.

JSON/default/validate tags unchanged; name/category/desc now carry config.* keys for translation. Looks good.

internal/handler/settings_handler.go (2)

5-5: New i18n import is appropriate.


21-35: Missing-translation fallback (verify).

Confirm i18n.Message returns the key or a sensible default when a translation is missing to prevent leaking raw keys in API responses.

#!/bin/bash
# Grep for i18n.Message implementation and check fallback behavior quickly
rg -nP 'func\s+Message\(' internal/i18n
rg -nP 'Localizer|MustLocalize|Localize' internal/i18n -C2
internal/handler/task_handler.go (1)

14-15: Localized error mapping — LGTM; verify key coverage.

The switch to ErrorI18nFromAPIError with "task.get_status_failed" looks good.

Run to confirm the translation key exists across all backend locales:

#!/bin/bash
rg -n "task.get_status_failed" internal/i18n/locales --glob '!**/vendor/**'
internal/handler/key_handler.go (5)

146-147: Good i18n validation error.

Using ErrorI18nFromAPIError with "validation.invalid_status_filter" is consistent with the new helpers.


286-287: Good i18n for group-not-found.

Switch to "validation.group_not_found" is correct.


325-326: Good i18n for invalid status value.

Consistent with list filter handling.


336-337: Good i18n for group-not-found (manual validation).

Matches TestMultipleKeys path.


428-429: Good i18n for export status filter.

Keeps validation consistent across endpoints.

internal/response/response.go (1)

42-68: New i18n response helpers — LGTM; ensure i18n.Message exists and resolves Accept-Language.

Helpers look correct and align with APIError.

Verify a function i18n.Message(*gin.Context, string, ...map[string]interface{}) string exists and uses a Localizer derived from the request:

#!/bin/bash
rg -nP 'func\s+Message\s*\(\s*\*gin\.Context' internal/i18n -C2
internal/handler/group_handler.go (4)

186-187: i18n-izing create-group validation — LGTM.

All key validations now return localized errors with appropriate template data.

Also applies to: 193-195, 199-201, 216-219, 236-239


327-328: i18n-izing update-group validation — LGTM.

Validation messages are now localized with template data where needed.

Also applies to: 354-356, 381-383, 393-394, 403-404, 439-441


871-875: Database error i18n — LGTM.

On stats failure, responding with "database.group_stats_failed" is appropriate.


680-689: Nice touch on translating config option labels/descriptions.

Runtime translation based on "config.*" prefixes is clean and keeps server-driven labels in sync.

internal/handler/dashboard_handler.go (2)

89-96: Dashboard i18n integration — LGTM.

  • Using i18n for stat labels and security warnings is consistent.
  • Passing gin.Context into helpers is the right direction.

Also applies to: 162-168, 231-258, 269-319


145-149: Confirm Go toolchain supports for i := range 24.

This requires Go 1.22+. Make sure go.mod declares Go 1.22+.

#!/bin/bash
# Expect "go 1.22" (or higher) or a toolchain directive
rg -n '^go\s+1\.(\d+)' go.mod -N
rg -n '^toolchain\s+' go.mod -N || true

Comment on lines 112 to 129
// T 翻译消息
func T(localizer *i18n.Localizer, msgID string, data ...map[string]interface{}) string {
config := &i18n.LocalizeConfig{
MessageID: msgID,
}

if len(data) > 0 {
config.TemplateData = data[0]
}

msg, err := localizer.Localize(config)
if err != nil {
// 如果翻译失败,返回消息ID
return msgID
}

return msg
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add i18n.Message that pulls Localizer from gin.Context and adapts {name} placeholders.

This centralizes context handling and mitigates the {name} vs {{.name}} mismatch until locales are updated.

Apply:

@@
-import (
+import (
   "encoding/json"
   "fmt"
   "strings"
+  "github.com/gin-gonic/gin"
@@
 func T(localizer *i18n.Localizer, msgID string, data ...map[string]interface{}) string {
@@
 }
 
+// Message 从 gin.Context 获取本地化器,并支持 {name} 占位符的后处理替换
+func Message(c *gin.Context, msgID string, data ...map[string]interface{}) string {
+  // Try context-stored localizer; fallback to header
+  var loc *i18n.Localizer
+  if v, ok := c.Get("localizer"); ok {
+    if l, ok := v.(*i18n.Localizer); ok {
+      loc = l
+    }
+  }
+  if loc == nil {
+    loc = GetLocalizer(c.GetHeader("Accept-Language"))
+  }
+  msg := T(loc, msgID, data...)
+  // Fallback replacement for {name} style placeholders
+  if len(data) > 0 && len(data[0]) > 0 {
+    for k, v := range data[0] {
+      msg = strings.ReplaceAll(msg, "{"+k+"}", fmt.Sprint(v))
+    }
+  }
+  return msg
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// T 翻译消息
func T(localizer *i18n.Localizer, msgID string, data ...map[string]interface{}) string {
config := &i18n.LocalizeConfig{
MessageID: msgID,
}
if len(data) > 0 {
config.TemplateData = data[0]
}
msg, err := localizer.Localize(config)
if err != nil {
// 如果翻译失败,返回消息ID
return msgID
}
return msg
}
// --- update the import block to include Gin ---
import (
"encoding/json"
"fmt"
"strings"
"github.com/gin-gonic/gin"
)
// T 翻译消息
func T(localizer *i18n.Localizer, msgID string, data ...map[string]interface{}) string {
config := &i18n.LocalizeConfig{
MessageID: msgID,
}
if len(data) > 0 {
config.TemplateData = data[0]
}
msg, err := localizer.Localize(config)
if err != nil {
// 如果翻译失败,返回消息ID
return msgID
}
return msg
}
// Message 从 gin.Context 获取本地化器,并支持 {name} 占位符的后处理替换
func Message(c *gin.Context, msgID string, data ...map[string]interface{}) string {
// Try context-stored localizer; fallback to header
var loc *i18n.Localizer
if v, ok := c.Get("localizer"); ok {
if l, ok := v.(*i18n.Localizer); ok {
loc = l
}
}
if loc == nil {
loc = GetLocalizer(c.GetHeader("Accept-Language"))
}
// Perform the usual i18n lookup
msg := T(loc, msgID, data...)
// Fallback replacement for {name}-style placeholders
if len(data) > 0 && len(data[0]) > 0 {
for k, v := range data[0] {
msg = strings.ReplaceAll(msg, "{"+k+"}", fmt.Sprint(v))
}
}
return msg
}

Comment on lines 47 to 55
"validation.invalid_group_name": "Invalid group name. Can only contain lowercase letters, numbers, hyphens or underscores, 1-100 characters",
"validation.invalid_test_path": "Invalid test path. If provided, must be a valid path starting with / and not a full URL.",
"validation.duplicate_header": "Duplicate header: {key}",
"validation.group_not_found": "Group not found",
"validation.invalid_status_filter": "Invalid status filter",
"validation.invalid_group_id": "Invalid group ID format",
"validation.test_model_required": "Test model is required",
"validation.invalid_copy_keys_value": "Invalid copy_keys value. Must be 'none', 'valid_only', or 'all'",
"validation.invalid_channel_type": "Invalid channel type. Supported types: {types}",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Same placeholder issue as zh-CN: adopt {{.name}} or add adapter.

Strings like "Duplicate header: {key}" and "invalid upstream URL format: {url}" won’t interpolate.

Apply the same fix strategy as noted for zh-CN.

Also applies to: 90-98, 145-154

🤖 Prompt for AI Agents
In internal/i18n/locales/en-US.go around lines 47 to 55 (and also apply same
changes to lines 90-98 and 145-154), the message placeholders use brace-style
tokens like {key} and {url} which will not interpolate with our Go template
rendering; update those strings to use the Go template placeholder format (e.g.,
{{.key}}, {{.url}}, or {{.name}}) or ensure they pass through the same adapter
used for zh-CN so placeholders are rendered; make the replacements consistently
across the listed ranges and verify plural/formatting patterns remain correct.

Comment on lines 49 to 56
"validation.duplicate_header": "重复的请求头: {key}",
"validation.group_not_found": "分组不存在",
"validation.invalid_status_filter": "无效的状态过滤器",
"validation.invalid_group_id": "无效的分组ID格式",
"validation.test_model_required": "测试模型是必需的",
"validation.invalid_copy_keys_value": "无效的copy_keys值。必须是'none'、'valid_only'或'all'",
"validation.invalid_channel_type": "无效的通道类型。支持的类型有: {types}",
"validation.test_model_empty": "测试模型不能为空或只有空格",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Placeholders won't interpolate with go-i18n; replace {var} with {{.var}} or add a fallback adapter.

go-i18n v2 expects template placeholders like {{.key}}. Using {key}, {count}, {url}, {field}, etc. will render literally (no substitution).

Two options:

  • Preferred: Change all placeholders to {{.name}} (e.g., 重复的请求头: {{.key}}).
  • Fast mitigation: Implement i18n.Message to post-process {name} tokens, replacing them from TemplateData. See my i18n.go suggestion.

Also applies to: 90-98, 145-154

🤖 Prompt for AI Agents
internal/i18n/locales/zh-CN.go lines 49-56 (and similarly apply to 90-98 and
145-154): the current message placeholders use {name} style which go-i18n v2
will not interpolate; replace all brace-style tokens like {key}, {types},
{count}, {url}, {field} with go-i18n template placeholders {{.key}}, {{.types}},
{{.count}}, {{.url}}, {{.field}} respectively (or consistently update all
occurrences in these ranges) so the TemplateData values are rendered;
alternatively, if you prefer a faster mitigation, implement a Message adapter
that post-processes string values to replace {name} tokens from TemplateData,
but the preferred fix is to edit the strings in-place to the {{.var}} form.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (10)
web/src/components/BaseInfoCard.vue (2)

147-151: Safe access for request_count value

Avoid accessing .value when request_count may be undefined.

Apply:

-                {{ stats ? formatValue(stats.request_count.value) : "--" }}
+                {{ stats ? formatValue(stats?.request_count?.value ?? 0) : "--" }}

171-177: Null-safe error_rate usage in v-if and formatter

Chain optional access and provide a fallback for formatTrend.

Apply:

-              <n-tag
-                v-if="stats?.error_rate.trend !== 0"
+              <n-tag
+                v-if="(stats?.error_rate?.trend ?? 0) !== 0"
                 :type="stats?.error_rate.trend_is_growth ? 'success' : 'error'"
                 size="small"
                 class="stat-trend"
               >
-                {{ stats ? formatTrend(stats.error_rate.trend) : "--" }}
+                {{ stats ? formatTrend(stats?.error_rate?.trend ?? 0) : "--" }}
web/src/api/logs.ts (1)

16-45: Prefer axios blob download to preserve headers (incl. Accept-Language) and handle errors gracefully.

Direct click bypasses axios interceptors and custom headers; switching enables consistent i18n and better error handling.

-  exportLogs: (params: Omit<LogFilter, "page" | "page_size">) => {
+  exportLogs: async (params: Omit<LogFilter, "page" | "page_size">) => {
     const authKey = localStorage.getItem("authKey");
     if (!authKey) {
       window.$message.error(i18n.global.t("auth.noAuthKeyFound"));
       return;
     }
-
-    const queryParams = new URLSearchParams(
-      Object.entries(params).reduce(
-        (acc, [key, value]) => {
-          if (value !== undefined && value !== null && value !== "") {
-            acc[key] = String(value);
-          }
-          return acc;
-        },
-        {} as Record<string, string>
-      )
-    );
-    queryParams.append("key", authKey);
-
-    const url = `${http.defaults.baseURL}/logs/export?${queryParams.toString()}`;
-
-    const link = document.createElement("a");
-    link.href = url;
-    link.setAttribute("download", `logs-${Date.now()}.csv`);
-    document.body.appendChild(link);
-    link.click();
-    document.body.removeChild(link);
+    try {
+      const res = await http.get("/logs/export", {
+        params: { ...params, key: authKey /* optionally: lang: i18n.global.locale?.value */ },
+        responseType: "blob",
+      });
+      // Derive filename
+      const cd = res.headers["content-disposition"] as string | undefined;
+      const m = cd && cd.match(/filename="?([^"]+)"?/i);
+      const filename = m?.[1] ?? `logs-${Date.now()}.csv`;
+      // Download
+      const blob = new Blob([res.data], { type: "text/csv;charset=utf-8" });
+      const url = window.URL.createObjectURL(blob);
+      const a = document.createElement("a");
+      a.href = url;
+      a.download = filename;
+      document.body.appendChild(a);
+      a.click();
+      a.remove();
+      window.URL.revokeObjectURL(url);
+    } catch (e: any) {
+      // Handle JSON error payload returned with 4xx/5xx
+      window.$message.error(i18n.global.t("error.export_logs"));
+    }
   },

Note: If you want UI-selected language to drive server locale for downloads regardless of browser Accept-Language, append lang=i18n.global.locale?.value and let the backend middleware honor it.

web/src/components/GlobalTaskProgressBar.vue (1)

105-111: Bug: clearInterval used for a setTimeout handle

Use clearTimeout to stop the timer; clearInterval is incorrect here.

Apply:

 function stopPolling() {
   isPolling = false;
   if (pollTimer) {
-    clearInterval(pollTimer);
+    clearTimeout(pollTimer);
     pollTimer = null;
   }
 }
web/src/components/AppFooter.vue (1)

31-53: Non-reactive status text; compute from status instead

statusConfig captures t() once, so text won’t change when locale changes. Keep color/icon static, compute text per render.

-const statusConfig = {
-  checking: { color: "#0066cc", icon: TimeOutline, text: t("footer.checking") },
-  latest: { color: "#18a058", icon: CheckmarkCircleOutline, text: t("footer.latestVersion") },
-  "update-available": { color: "#f0a020", icon: WarningOutline, text: t("footer.updateAvailable") },
-  error: { color: "#d03050", icon: WarningOutline, text: t("footer.checkFailed") },
-};
+const statusConfig = {
+  checking: { color: "#0066cc", icon: TimeOutline },
+  latest: { color: "#18a058", icon: CheckmarkCircleOutline },
+  "update-available": { color: "#f0a020", icon: WarningOutline },
+  error: { color: "#d03050", icon: WarningOutline },
+} as const;
+
+function getStatusText(status: keyof typeof statusConfig) {
+  switch (status) {
+    case "checking":
+      return t("footer.checking");
+    case "latest":
+      return t("footer.latestVersion");
+    case "update-available":
+      return t("footer.updateAvailable");
+    default:
+      return t("footer.checkFailed");
+  }
+}

And in template:

-{{ statusConfig[versionInfo.status].text }}
+{{ getStatusText(versionInfo.status as any) }}
web/src/components/keys/GroupInfoCard.vue (3)

140-154: Handle loadStats errors to avoid unhandled rejections.

Currently try/finally without catch will still throw. Show a localized toast and null-out stats on failure.

Apply:

 async function loadStats() {
   if (!props.group?.id) {
     stats.value = null;
     return;
   }

   try {
     loading.value = true;
-    if (props.group?.id) {
-      stats.value = await keysApi.getGroupStats(props.group.id);
-    }
+    if (props.group?.id) {
+      stats.value = await keysApi.getGroupStats(props.group.id);
+    } else {
+      stats.value = null;
+    }
+  } catch (err) {
+    stats.value = null;
+    console.error("Failed to load group stats:", err);
+    window.$message?.error(t("keys.loadStatsFailed"));
   } finally {
     loading.value = false;
   }
 }

263-269: Return "0%" instead of "0" for percentages.

 function formatPercentage(num: number): string {
   if (num <= 0) {
-    return "0";
+    return "0%";
   }
   return `${(num * 100).toFixed(1)}%`;
 }

67-73: Accurate "advanced config" visibility checks.

param_overrides and header_rules should be considered “present” only when non-empty.

 const hasAdvancedConfig = computed(() => {
-  return (
-    (props.group?.config && Object.keys(props.group.config).length > 0) ||
-    props.group?.param_overrides ||
-    (props.group?.header_rules && props.group.header_rules.length > 0)
-  );
+  const hasConfig = !!props.group?.config && Object.keys(props.group.config).length > 0;
+  const hasOverrides =
+    !!props.group?.param_overrides && Object.keys(props.group.param_overrides).length > 0;
+  const hasHeaders = !!props.group?.header_rules && props.group.header_rules.length > 0;
+  return hasConfig || hasOverrides || hasHeaders;
 });
-                  <n-form-item
-                    v-if="group?.param_overrides"
+                  <n-form-item
+                    v-if="group?.param_overrides && Object.keys(group.param_overrides).length > 0"
                     :label="`${t('keys.paramOverrides')}:`"
                     :span="2"
                   >

Also applies to: 599-602

internal/handler/group_handler.go (1)

368-375: Unify validation error i18n for upstreams in UpdateGroup.

CreateGroup returns a localized invalid_upstreams error; UpdateGroup should match for consistency.

-    if err != nil {
-      response.Error(c, app_errors.NewAPIError(app_errors.ErrValidation, err.Error()))
-      return
-    }
+    if err != nil {
+      response.ErrorI18nFromAPIError(
+        c,
+        app_errors.ErrValidation,
+        "validation.invalid_upstreams",
+        map[string]any{"error": err.Error()},
+      )
+      return
+    }
internal/handler/dashboard_handler.go (1)

145-156: Bug: invalid Go for-loop (for i := range 24).

Range over int is illegal. Use a classic counted loop.

- for i := range 24 {
+ for i := 0; i < 24; i++ {
   hour := startHour.Add(time.Duration(i) * time.Hour)
   labels = append(labels, hour.Format(time.RFC3339))

   if data, ok := statsByHour[hour]; ok {
     successData = append(successData, data["success"])
     failureData = append(failureData, data["failure"])
   } else {
     successData = append(successData, 0)
     failureData = append(failureData, 0)
   }
 }
♻️ Duplicate comments (10)
web/src/components/BaseInfoCard.vue (1)

115-117: Guard toFixed against undefined

Calling toFixed on possibly undefined will throw.

Apply:

-                {{ stats?.rpm?.value.toFixed(1) ?? 0 }}
+                {{ (stats?.rpm?.value ?? 0).toFixed(1) }}
internal/i18n/locales/en-US.go (1)

47-63: Placeholders fixed to {{.var}} — resolved

Previous brace-style placeholders are now correct. 👍

web/src/locales/ja-JP.ts (1)

486-487: Typo fix: “闾値” → “閾値”.

User-facing typo in the task validation completion string.

Apply:

-      "キー検証完了、{total}個のキーを処理、{valid}個成功、{invalid}個失敗。注意:検証失敗でもすぐにブラックリストに追加されるわけではありません。失敗回数が闾値に達する必要があります。",
+      "キー検証完了、{total}個のキーを処理、{valid}個成功、{invalid}個失敗。注意:検証失敗でもすぐにブラックリストに追加されるわけではありません。失敗回数が閾値に達する必要があります。",
web/src/locales/en-US.ts (1)

149-151: Polish wording: “Separator separated” → “Delimiter separated”.

Aligns with standard terminology.

-    separated: "Separator separated",
+    separated: "Delimiter separated",
web/src/components/keys/KeyTable.vue (2)

58-62: Make statusOptions reactive to locale changes (computed)

Currently labels are frozen; convert to computed so t() re-evaluates on locale switch. Same ask was raised earlier.

-const statusOptions = [
-  { label: t("common.all"), value: "all" },
-  { label: t("keys.valid"), value: "active" },
-  { label: t("keys.invalid"), value: "invalid" },
-];
+const statusOptions = computed(() => [
+  { label: t("common.all"), value: "all" },
+  { label: t("keys.valid"), value: "active" },
+  { label: t("keys.invalid"), value: "invalid" },
+]);

65-85: Same for moreOptions: compute so it reacts to locale

Otherwise the dropdown labels won’t update on language switch.

-const moreOptions = [
+const moreOptions = computed(() => [
   { label: t("keys.exportAllKeys"), key: "copyAll" },
   { label: t("keys.exportValidKeys"), key: "copyValid" },
   { label: t("keys.exportInvalidKeys"), key: "copyInvalid" },
   { type: "divider" },
   { label: t("keys.restoreAllInvalidKeys"), key: "restoreAll" },
   {
     label: t("keys.clearAllInvalidKeys"),
     key: "clearInvalid",
     props: { style: { color: "#d03050" } },
   },
   {
     label: t("keys.clearAllKeys"),
     key: "clearAll",
     props: { style: { color: "red", fontWeight: "bold" } },
   },
   { type: "divider" },
   { label: t("keys.validateAllKeys"), key: "validateAll" },
   { label: t("keys.validateValidKeys"), key: "validateActive" },
   { label: t("keys.validateInvalidKeys"), key: "validateInvalid" },
-];
+]);

Note: No template changes needed; Vue unwraps computed in templates.

internal/i18n/i18n.go (2)

18-32: Make Init idempotent and thread-safe (sync.Once)

Prevents double-loads and races when multiple callers invoke Init.

 var (
-	bundle *i18n.Bundle
+	bundle   *i18n.Bundle
+	initOnce sync.Once
+	initErr  error
 )
 
 // Init 初始化 i18n
 func Init() error {
-	bundle = i18n.NewBundle(language.Chinese)
-	bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
-
-	// 加载支持的语言文件
-	languages := []string{"zh-CN", "en-US", "ja-JP"}
-	for _, lang := range languages {
-		if err := loadMessageFile(lang); err != nil {
-			return fmt.Errorf("failed to load language file %s: %w", lang, err)
-		}
-	}
-
-	return nil
+	initOnce.Do(func() {
+		bundle = i18n.NewBundle(language.Chinese)
+		bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
+		langs := []string{"zh-CN", "en-US", "ja-JP"}
+		for _, lang := range langs {
+			if err := loadMessageFile(lang); err != nil {
+				initErr = fmt.Errorf("failed to load language file %s: %w", lang, err)
+				return
+			}
+		}
+	})
+	return initErr
 }

Add import:

-import (
+import (
   "encoding/json"
   "fmt"
   "strings"
+  "sync"

48-59: Defensive: lazy-init bundle in GetLocalizer

Avoid nil-deref if called before Init (tests, mis-ordered startup).

 func GetLocalizer(acceptLang string) *i18n.Localizer {
+	// Ensure initialized
+	if bundle == nil {
+		_ = Init()
+	}
 	// 解析 Accept-Language 头
 	langs := parseAcceptLanguage(acceptLang)
web/src/components/logs/LogTable.vue (1)

67-75: Make filter option labels reactive to locale changes

Use computed so labels update when the user switches language.

-const successOptions = [
-  { label: t("common.success"), value: "true" },
-  { label: t("common.error"), value: "false" },
-];
-
-const requestTypeOptions = [
-  { label: t("logs.retryRequest"), value: "retry" },
-  { label: t("logs.finalRequest"), value: "final" },
-];
+const successOptions = computed(() => [
+  { label: t("common.success"), value: "true" },
+  { label: t("common.error"), value: "false" },
+]);
+
+const requestTypeOptions = computed(() => [
+  { label: t("logs.retryRequest"), value: "retry" },
+  { label: t("logs.finalRequest"), value: "final" },
+]);
web/src/locales/index.ts (1)

16-41: Harden default-locale resolution for SSR/private mode

Guard localStorage/navigator access and fall back safely.

 function getDefaultLocale(): Locale {
-  // 1. 优先使用 localStorage 中保存的语言
-  const savedLocale = localStorage.getItem("locale");
-  if (savedLocale && SUPPORTED_LOCALES.some(l => l.key === savedLocale)) {
-    return savedLocale as Locale;
-  }
-
-  // 2. 自动检测浏览器语言
-  const browserLang = navigator.language;
+  try {
+    const savedLocale =
+      typeof window !== "undefined" ? localStorage.getItem("locale") : null;
+    if (savedLocale && SUPPORTED_LOCALES.some(l => l.key === savedLocale)) {
+      return savedLocale as Locale;
+    }
+    const browserLang =
+      typeof navigator !== "undefined" && navigator.language ? navigator.language : "";
 
-  // 精确匹配
-  if (SUPPORTED_LOCALES.some(l => l.key === browserLang)) {
-    return browserLang as Locale;
-  }
+    if (SUPPORTED_LOCALES.some(l => l.key === browserLang)) {
+      return browserLang as Locale;
+    }
 
-  // 模糊匹配(如 zh 匹配 zh-CN)
-  const shortLang = browserLang.split("-")[0];
-  const matched = SUPPORTED_LOCALES.find(l => l.key.startsWith(shortLang));
-  if (matched) {
-    return matched.key;
-  }
+    const shortLang = browserLang.split("-")[0];
+    const matched = SUPPORTED_LOCALES.find(l => l.key.startsWith(shortLang));
+    if (matched) return matched.key;
+  } catch { /* noop */ }
 
   // 3. 默认中文
   return "zh-CN";
 }
🧹 Nitpick comments (53)
web/package.json (1)

31-31: Fix npm script chaining

The script won’t run sub-scripts without “run”.

Apply:

-"check-all": "npm lint:check && npm format:check && npm type-check"
+"check-all": "npm run lint:check && npm run format:check && npm run type-check"
internal/i18n/locales/ja-JP.go (2)

6-8: Unify “success” strings

You have both "success": "操作成功" and "common.success": "成功". Consider aligning "success" to "成功" for consistency with other locales.

-	"success":        "操作成功",
+	"success":        "成功",

95-103: Optional native review for tone

Some phrases read as direct translations; a quick native pass could improve fluency (no blockers).

web/src/components/common/ProxyKeysInput.vue (1)

34-41: Prefer Web Crypto for key generation randomness

Math.random is predictable. Use crypto.getRandomValues when available, fallback otherwise.

Apply:

-function generateRandomString(length: number): string {
-  const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
-  let result = "";
-  for (let i = 0; i < length; i++) {
-    result += chars.charAt(Math.floor(Math.random() * chars.length));
-  }
-  return result;
-}
+function generateRandomString(length: number): string {
+  const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+  if (typeof crypto !== "undefined" && "getRandomValues" in crypto) {
+    const bytes = new Uint8Array(length);
+    crypto.getRandomValues(bytes);
+    return Array.from(bytes, b => chars[b % chars.length]).join("");
+  }
+  // Fallback
+  let result = "";
+  for (let i = 0; i < length; i++) {
+    result += chars.charAt(Math.floor(Math.random() * chars.length));
+  }
+  return result;
+}
web/src/locales/zh-CN.ts (1)

113-115: Combine split confirmation strings into one templated key

Concatenating “危险操作” + “下的所有密钥…” can be awkward across locales. Prefer a single string with placeholders to avoid word-order issues.

Example:

-    dangerousOperation: "这是一个危险的操作,将删除分组 ",
-    toConfirmDeletion: " 下的所有密钥。请输入分组名称以确认:",
+    confirmDeleteGroupWithHint: "这是一个危险操作,将删除分组「{name}」下的所有密钥。请输入分组名称以确认:",
internal/types/types.go (2)

22-26: Add URL validation to strengthen config sanity.

Recommend validating URLs where applicable.

-	AppUrl                         string `json:"app_url" default:"http://localhost:3001" name:"config.app_url" category:"config.category.basic" desc:"config.app_url_desc" validate:"required"`
+	AppUrl                         string `json:"app_url" default:"http://localhost:3001" name:"config.app_url" category:"config.category.basic" desc:"config.app_url_desc" validate:"required,url"`

29-36: Validate optional ProxyURL format without forcing presence.

If ProxyURL is provided, ensure it’s a valid URL.

-	ProxyURL              string `json:"proxy_url" name:"config.proxy_url" category:"config.category.request" desc:"config.proxy_url_desc"`
+	ProxyURL              string `json:"proxy_url" name:"config.proxy_url" category:"config.category.request" desc:"config.proxy_url_desc" validate:"omitempty,url"`
internal/router/router.go (2)

176-181: Localize 405 response text.

Return translated message using request’s Accept-Language so users see localized errors even outside /api.

 router.NoMethod(func(c *gin.Context) {
-    c.JSON(http.StatusMethodNotAllowed, gin.H{"error": "Method not allowed"})
+    // Use Accept-Language directly since /api group middleware may not run here
+    msg := i18n.T(i18n.GetLocalizer(c.GetHeader("Accept-Language")), "http.method_not_allowed")
+    c.JSON(http.StatusMethodNotAllowed, gin.H{"error": msg})
 })

186-191: Localize 404 response text for API/proxy misses.

Ensure consistent i18n for Not Found responses.

  router.NoRoute(func(c *gin.Context) {
     if strings.HasPrefix(c.Request.RequestURI, "/api") || strings.HasPrefix(c.Request.RequestURI, "/proxy") {
-        c.JSON(http.StatusNotFound, gin.H{"error": "Not Found"})
+        msg := i18n.T(i18n.GetLocalizer(c.GetHeader("Accept-Language")), "http.not_found")
+        c.JSON(http.StatusNotFound, gin.H{"error": msg})
         return
     }

Tip: consider letting the i18n middleware also honor a "lang" query param or cookie to cover download links that can’t set headers.

web/src/components/Logout.vue (1)

19-24: Minor a11y: add an aria-label.

Improves screen reader support.

-  <n-button quaternary round class="logout-button" @click="handleLogout">
+  <n-button
+    quaternary
+    round
+    class="logout-button"
+    :aria-label="t('nav.logout')"
+    @click="handleLogout"
+  >
internal/handler/handler.go (1)

119-123: Optional: localize health status string.

Keeps system endpoints consistent with i18n.

-    "status":    "healthy",
+    "status":    i18n.Message(c, "system.healthy"),
internal/handler/log_handler.go (1)

3-14: Unify logging: use logrus consistently.

Since logrus is already imported, drop std log import to avoid split logging sinks.

 import (
   "fmt"
   app_errors "gpt-load/internal/errors"
   "gpt-load/internal/i18n"
   "gpt-load/internal/models"
   "gpt-load/internal/response"
-  "log"
   "time"
internal/handler/settings_handler.go (1)

21-35: DRY the repeated prefix checks; centralize with a tiny helper.

Same logic repeated for Name/Description/Category. A local helper reduces duplication and the chance of inconsistent updates.

Apply within this block:

-	// Translate settings info
-	for i := range settingsInfo {
-		// Translate name if it's an i18n key
-		if strings.HasPrefix(settingsInfo[i].Name, "config.") {
-			settingsInfo[i].Name = i18n.Message(c, settingsInfo[i].Name)
-		}
-		// Translate description if it's an i18n key
-		if strings.HasPrefix(settingsInfo[i].Description, "config.") {
-			settingsInfo[i].Description = i18n.Message(c, settingsInfo[i].Description)
-		}
-		// Translate category if it's an i18n key
-		if strings.HasPrefix(settingsInfo[i].Category, "config.") {
-			settingsInfo[i].Category = i18n.Message(c, settingsInfo[i].Category)
-		}
-	}
+	// Translate settings info
+	localizeIfKey := func(v string) string {
+		if strings.HasPrefix(v, "config.") {
+			return i18n.Message(c, v)
+		}
+		return v
+	}
+	for i := range settingsInfo {
+		settingsInfo[i].Name = localizeIfKey(settingsInfo[i].Name)
+		settingsInfo[i].Description = localizeIfKey(settingsInfo[i].Description)
+		settingsInfo[i].Category = localizeIfKey(settingsInfo[i].Category)
+	}

Note: Grouping by localized Category changes the map key per locale. Verify this is intended for the client response.

web/src/components/NavBar.vue (1)

5-8: Make labels robustly reactive to locale changes.

t(...) inside a computed usually tracks locale, but some menu libs cache options. Safer to resolve labels at render time.

Minimal change in this block:

-    renderMenuItem("dashboard", t("nav.dashboard"), "📊"),
-    renderMenuItem("keys", t("nav.keys"), "🔑"),
-    renderMenuItem("logs", t("nav.logs"), "📋"),
-    renderMenuItem("settings", t("nav.settings"), "⚙️"),
+    renderMenuItem("dashboard", "nav.dashboard", "📊"),
+    renderMenuItem("keys", "nav.keys", "🔑"),
+    renderMenuItem("logs", "nav.logs", "📋"),
+    renderMenuItem("settings", "nav.settings", "⚙️"),

And update the helper (outside this hunk) to resolve the label at render:

function renderMenuItem(key: string, labelKey: string, icon: string): MenuOption {
  return {
    label: () =>
      h(
        RouterLink,
        { to: { name: key }, class: "nav-menu-item" },
        { default: () => [h("span", { class: "nav-item-icon" }, icon), h("span", { class: "nav-item-text" }, t(labelKey))] }
      ),
    key,
  };
}

Please verify the menu updates immediately after switching languages.

Also applies to: 20-23

web/src/components/Layout.vue (1)

31-31: Minor a11y: add a meaningful alt to the brand logo (and localize if desired).

Example:

<img src="@/assets/logo.png" :alt="`GPT Load`" />

Or use a localized string if you have one in locales (e.g., common.appName).

web/src/locales/ja-JP.ts (1)

331-334: Word choice nits for short labels.

Prefer commonly used katakana in UI for consistency.

-    requestsShort: "要求",
+    requestsShort: "リクエスト",
-    testShort: "試験",
+    testShort: "テスト",
web/src/components/ThemeToggle.vue (1)

44-44: Add aria-label for the toggle button (localized).

Example:

<n-button quaternary circle @click="toggleTheme" :aria-label="t('theme.clickToSwitch', { mode: themeConfig.nextMode })">
web/src/components/keys/KeyDeleteDialog.vue (1)

99-101: Disable button should respect trimmed input.

The submit guard uses keysText.value.trim(), but the button enables on whitespace. Align the disabled state with the trimmed check.

Apply:

-          <n-button type="error" @click="handleSubmit" :loading="loading" :disabled="!keysText">
+          <n-button type="error" @click="handleSubmit" :loading="loading" :disabled="!keysText?.trim()">
web/src/components/keys/KeyCreateDialog.vue (1)

97-100: Match trimmed validation in disabled state.

Prevent submitting whitespace-only input.

-          <n-button type="primary" @click="handleSubmit" :loading="loading" :disabled="!keysText">
+          <n-button type="primary" @click="handleSubmit" :loading="loading" :disabled="!keysText?.trim()">
web/src/components/LineChart.vue (1)

217-218: Don’t infer “error” datasets via localized label substrings.

Label-based detection breaks with translations and casing. Use a metadata flag from backend if available; otherwise make the check locale-agnostic.

Apply a safer fallback:

-  return label.includes("失败") || label.includes("Error") || label.includes("エラー");
+  return /(失败|error|エラー)/i.test(label);

If possible, prefer dataset.isError from the API over label parsing.

web/src/components/LanguageSelector.vue (1)

25-34: Add an accessible label to the button.

Improves screen reader support without UI changes.

-    <n-button quaternary size="medium" class="language-selector-btn">
+    <n-button
+      quaternary
+      size="medium"
+      class="language-selector-btn"
+      :aria-label="`Language: ${currentLabel}`"
+    >
web/src/utils/http.ts (1)

31-34: Use the active i18n locale as the single source of truth for Accept-Language.

Defaulting to "zh-CN" can disagree with the UI’s initial locale. Prefer i18n.global.locale (fall back to localStorage).

-  // 添加语言头
-  const locale = localStorage.getItem("locale") || "zh-CN";
-  config.headers["Accept-Language"] = locale;
+  // 添加语言头(优先使用当前 i18n 语言,其次 localStorage,最后默认)
+  const activeLocale =
+    ((i18n.global as any).locale?.value as string) ||
+    localStorage.getItem("locale") ||
+    "zh-CN";
+  config.headers["Accept-Language"] = activeLocale;
web/src/views/Settings.vue (1)

62-68: Add a success toast after saving settings.

Provides clear feedback; backend may omit a message for PATCH/PUT.

   await settingsApi.updateSettings(form.value);
   await fetchSettings();
+  message.success(t("settings.settingsSaved"));
web/src/locales/en-US.ts (3)

112-116: Fix grammar in confirmation prompts.

More natural English phrasing.

-    confirmDeleteGroup: "Are you sure to delete group {name}?",
+    confirmDeleteGroup: "Are you sure you want to delete group {name}?",
...
-    confirmDeleteKey: 'Are you sure to delete key "{key}"?',
+    confirmDeleteKey: 'Are you sure you want to delete the key "{key}"?',

Also applies to: 174-176


113-115: Avoid sentence-splitting across keys for dangerous operation text.

Concatenated sentences are fragile for translation. Prefer a single message with placeholders.

Example addition (keep existing keys for now; migrate progressively):

dangerousOperationFull: 'This is a dangerous operation that will delete group "{name}" and all its keys. Please enter the group name to confirm:'

Update UI to use keys.dangerousOperationFull with {name}.


356-357: Clarify wording.

“Failed to load logs request” → “Failed to load request logs”.

-    requestFailed: "Failed to load logs request",
+    requestFailed: "Failed to load request logs",
web/src/components/keys/GroupCopyModal.vue (2)

165-171: Disable confirm when no source group.

Avoids unnecessary error toast and improves UX.

-          <n-button type="primary" @click="handleCopy" :loading="loading">
+          <n-button
+            type="primary"
+            @click="handleCopy"
+            :loading="loading"
+            :disabled="!sourceGroup || loading"
+          >

145-145: Form label width may truncate in some languages.

80px can be tight for Japanese/English. Consider removing label-width to let it auto-size or using responsive styles.

web/src/components/GlobalTaskProgressBar.vue (1)

133-142: Avoid “undefined” in titles when group_name is absent

Pass a safe fallback to groupName to prevent literal “undefined” in UI.

-      return t("task.validatingKeys", { groupName: taskInfo.value.group_name });
+      return t("task.validatingKeys", { groupName: taskInfo.value.group_name || "" });
@@
-      return t("task.importingKeys", { groupName: taskInfo.value.group_name });
+      return t("task.importingKeys", { groupName: taskInfo.value.group_name || "" });
@@
-      return t("task.deletingKeys", { groupName: taskInfo.value.group_name });
+      return t("task.deletingKeys", { groupName: taskInfo.value.group_name || "" });
web/src/components/keys/KeyTable.vue (2)

88-90: Typo: isDeling → isDeleting

Keeps naming consistent and readable.

-const isDeling = ref(false);
+const isDeleting = ref(false);

Replace usages accordingly within this file (guards and finally blocks).


262-283: Optional: localize duration units (m/s/ms)

If desired, move units to i18n (e.g., time.minutesShort, time.secondsShort, time.msShort) for full localization.

internal/handler/key_handler.go (2)

48-59: Unify “group not found” to i18n

Return a localized message here for consistency with other paths.

 if err := s.DB.First(&group, groupID).Error; err != nil {
   if err == gorm.ErrRecordNotFound {
-    response.Error(c, app_errors.ErrResourceNotFound)
+    response.ErrorI18nFromAPIError(c, app_errors.ErrResourceNotFound, "validation.group_not_found")
   } else {
     response.Error(c, app_errors.ParseDBError(err))
   }
   return nil, false
 }

438-441: Unify logging: prefer logrus over log.Printf

Stay consistent with logrus usage elsewhere in this file.

-  if err := s.KeyService.StreamKeysToWriter(groupID, statusFilter, c.Writer); err != nil {
-    log.Printf("Failed to stream keys: %v", err)
-  }
+  if err := s.KeyService.StreamKeysToWriter(groupID, statusFilter, c.Writer); err != nil {
+    logrus.WithError(err).Error("Failed to stream keys")
+  }
web/src/components/logs/LogTable.vue (5)

162-195: Recompute columns on locale change

Column headers won’t update after a locale switch because columns is built once. Wrap in computed.

-const columns = createColumns();
+const columns = computed(createColumns);

Also applies to: 197-213, 232-252, 255-256


106-114: Use the injected message instance instead of window.$message

window.$message is brittle and can break SSR/tests. You already have message = useMessage().

-      window.$message.error(res.message || t("logs.loadFailed"), {
+      message.error(res.message || t("logs.loadFailed"), {
         keepAliveOnHover: true,
         duration: 5000,
         closable: true,
       });
@@
-    window.$message.error(t("logs.requestFailed"));
+    message.error(t("logs.requestFailed"));

207-213: Localize the “Key” column header

-  {
-    title: "Key",
+  {
+    title: t("logs.key"),
     key: "key_value",
     width: 200,

551-559: Localize copy toast context for API Key

-                      @click="copyContent(selectedLog.key_value, 'API Key')"
+                      @click="copyContent(selectedLog.key_value, t('logs.key'))"

609-617: Localize “User Agent” label and copy context

-                  <span class="compact-field-title">User Agent</span>
+                  <span class="compact-field-title">{{ t("logs.userAgent") }}</span>
@@
-                    @click="copyContent(selectedLog.user_agent, 'User Agent')"
+                    @click="copyContent(selectedLog.user_agent, t('logs.userAgent'))"

Also applies to: 615-616

web/src/components/keys/GroupFormModal.vue (2)

111-148: Deduplicate channel defaults with a single source of truth

Three switch blocks and two “old default” helpers drift easily. Centralize defaults and reference them.

+const CHANNEL_DEFAULTS = {
+  openai: { model: "gpt-4.1-nano", upstream: "https://api.openai.com", validation: "/v1/chat/completions" },
+  gemini: { model: "gemini-2.0-flash-lite", upstream: "https://generativelanguage.googleapis.com", validation: null },
+  anthropic: { model: "claude-3-haiku-20240307", upstream: "https://api.anthropic.com", validation: "/v1/messages" },
+} as const;
+
 const testModelPlaceholder = computed(() => {
-  switch (formData.channel_type) {
-    case "openai":
-      return "gpt-4.1-nano";
-    case "gemini":
-      return "gemini-2.0-flash-lite";
-    case "anthropic":
-      return "claude-3-haiku-20240307";
-    default:
-      return t("keys.enterModelName");
-  }
+  return CHANNEL_DEFAULTS[formData.channel_type]?.model || t("keys.enterModelName");
 });
 
 const upstreamPlaceholder = computed(() => {
-  switch (formData.channel_type) {
-    case "openai":
-      return "https://api.openai.com";
-    case "gemini":
-      return "https://generativelanguage.googleapis.com";
-    case "anthropic":
-      return "https://api.anthropic.com";
-    default:
-      return t("keys.enterUpstreamUrl");
-  }
+  return CHANNEL_DEFAULTS[formData.channel_type]?.upstream || t("keys.enterUpstreamUrl");
 });
 
 const validationEndpointPlaceholder = computed(() => {
-  switch (formData.channel_type) {
-    case "openai":
-      return "/v1/chat/completions";
-    case "anthropic":
-      return "/v1/messages";
-    case "gemini":
-      return ""; // Gemini 不显示此字段
-    default:
-      return t("keys.enterValidationPath");
-  }
+  const v = CHANNEL_DEFAULTS[formData.channel_type]?.validation;
+  return v ?? t("keys.enterValidationPath");
 });
@@
-        formData.test_model === getOldDefaultTestModel(oldChannelType)
+        formData.test_model === (CHANNEL_DEFAULTS[oldChannelType as keyof typeof CHANNEL_DEFAULTS]?.model || "")
@@
-          formData.upstreams[0].url === getOldDefaultUpstream(oldChannelType))
+          formData.upstreams[0].url === (CHANNEL_DEFAULTS[oldChannelType as keyof typeof CHANNEL_DEFAULTS]?.upstream || "")
@@
-function getOldDefaultTestModel(channelType: string): string {
-  switch (channelType) {
-    case "openai":
-      return "gpt-4.1-nano";
-    case "gemini":
-      return "gemini-2.0-flash-lite";
-    case "anthropic":
-      return "claude-3-haiku-20240307";
-    default:
-      return "";
-  }
-}
-
-function getOldDefaultUpstream(channelType: string): string {
-  switch (channelType) {
-    case "openai":
-      return "https://api.openai.com";
-    case "gemini":
-      return "https://generativelanguage.googleapis.com";
-    case "anthropic":
-      return "https://api.anthropic.com";
-    default:
-      return "";
-  }
-}
+// old-default helpers no longer needed

Also applies to: 209-233, 235-261


335-344: Add error handling for async option fetches

Surface failures to users so they know why selects are empty.

 async function fetchChannelTypes() {
-  const options = (await settingsApi.getChannelTypes()) || [];
-  channelTypeOptions.value =
-    options?.map((type: string) => ({
-      label: type,
-      value: type,
-    })) || [];
-  channelTypesFetched.value = true;
+  try {
+    const options = (await settingsApi.getChannelTypes()) || [];
+    channelTypeOptions.value =
+      options?.map((type: string) => ({ label: type, value: type })) || [];
+    channelTypesFetched.value = true;
+  } catch {
+    message.error(t("common.loadFailed"));
+  }
 }
@@
 async function fetchGroupConfigOptions() {
-  const options = await keysApi.getGroupConfigOptions();
-  configOptions.value = options || [];
-  configOptionsFetched.value = true;
+  try {
+    const options = await keysApi.getGroupConfigOptions();
+    configOptions.value = options || [];
+    configOptionsFetched.value = true;
+  } catch {
+    message.error(t("common.loadFailed"));
+  }
 }

Also applies to: 362-366

internal/i18n/middleware.go (2)

46-54: Default language mismatch risk

GetLangFromContext returns normalized Accept-Language, which may differ from the actual fallback chosen by GetLocalizer. Consider deriving lang from the resolved localizer or storing the normalized, supported value from normalizeLanguageCode(...).


56-56: Optional: use http.Status constants instead of magic numbers*

If you keep JSON helpers, prefer http.StatusOK etc.

web/src/locales/index.ts (2)

61-74: Avoid full page reload on locale change

Switch the runtime locale and header; no need to reload.

 export function setLocale(locale: Locale) {
   // 保存到 localStorage
   localStorage.setItem("locale", locale);
 
   // 更新 axios 的默认 headers
   if (axios.defaults.headers) {
     axios.defaults.headers.common["Accept-Language"] = locale;
   }
 
-  // 刷新页面以确保所有内容(包括后端数据)都使用新语言
-  window.location.reload();
+  // 切换运行时语言并更新 <html lang>
+  // @ts-ignore - vue-i18n typings
+  i18n.global.locale.value = locale;
+  if (typeof document !== "undefined") {
+    document.documentElement.lang = locale;
+  }
 }

56-59: Set on init for accessibility/SEO

Set it once on boot and keep it in sync on changes.

 const defaultLocale = getDefaultLocale();
 const i18n = createI18n({
@@
 });
 
 // 初始化时设置 axios 默认语言
 if (axios.defaults.headers) {
   axios.defaults.headers.common["Accept-Language"] = defaultLocale;
 }
+if (typeof document !== "undefined") {
+  document.documentElement.lang = defaultLocale;
+}

Also applies to: 61-74

web/src/components/keys/GroupInfoCard.vue (5)

156-163: Surface a user-facing error on config options load failure.

Add a localized toast; keep console error for diagnostics.

 async function loadConfigOptions() {
   try {
     const options = await keysApi.getGroupConfigOptions();
     configOptions.value = options || [];
   } catch (error) {
-    console.error("Failed to load config options:", error);
+    console.error("Failed to load config options:", error);
+    window.$message?.error(t("keys.loadConfigFailed"));
   }
 }

282-286: Reset more UI state on group switch.

Avoid leaking “show keys” or confirmation text between groups.

 function resetPage() {
   showEditModal.value = false;
   showCopyModal.value = false;
   expandedName.value = [];
+  showProxyKeys.value = false;
+  confirmInput.value = "";
 }

455-520: Localize punctuation; replace hardcoded full-width colon ":"

Embedding U+FF1A forces Chinese typography in all locales. Move the colon into translations (common.colon) or concatenate from i18n.

Example pattern (apply to all labels here and below):

-<n-form-item :label="`${t('keys.groupName')}:`">
+<n-form-item :label="`${t('keys.groupName')}${t('common.colon')}`">

Alternatively, store the colon inside each translation.


202-251: Trim confirmation input before comparing names.

Prevents false negatives due to accidental whitespace.

-          if (confirmInput.value !== props.group?.name) {
+          if (confirmInput.value.trim() !== props.group?.name) {
             window.$message.error(t("keys.incorrectGroupName"));
             return false; // Prevent dialog from closing
           }

253-261: Locale-aware number compact formatting (optional).

K-suffix isn’t localized. Consider Intl.NumberFormat with notation:"compact".

-function formatNumber(num: number): string {
-  if (num >= 1000) {
-    return `${(num / 1000).toFixed(1)}K`;
-  }
-  return num.toString();
-}
+function formatNumber(num: number): string {
+  try {
+    return Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1 }).format(
+      num
+    );
+  } catch {
+    return num.toString();
+  }
+}
internal/handler/group_handler.go (2)

599-613: Avoid external store calls inside DB transaction (atomicity vs. availability).

Calling Redis (RemoveKeysFromStore) while holding a DB tx risks long locks and partial failures. Prefer committing DB first, then best-effort cache/store cleanup with retry/queue (outbox pattern) and idempotent removals.

I can sketch an outbox + worker flow if helpful.


963-966: Localize the copy suffix for DisplayName.

Hardcoded " Copy" leaks English into data. Either drop the suffix or localize via i18n with placeholders.

-  if sourceGroup.DisplayName != "" {
-    newGroup.DisplayName = sourceGroup.DisplayName + " Copy"
-  }
+  if sourceGroup.DisplayName != "" {
+    newGroup.DisplayName = fmt.Sprintf("%s %s", sourceGroup.DisplayName, i18n.Message(c, "group.copy_suffix"))
+  }

Note: Requires passing c to this scope or deferring suffixing to the response layer.

internal/handler/dashboard_handler.go (2)

16-19: Optional: check DB errors for counts.

Counting active/invalid keys ignores potential DB errors; defaulting to zero silently could mislead dashboards.

 var activeKeys, invalidKeys int64
-s.DB.Model(&models.APIKey{}).Where("status = ?", models.KeyStatusActive).Count(&activeKeys)
-s.DB.Model(&models.APIKey{}).Where("status = ?", models.KeyStatusInvalid).Count(&invalidKeys)
+if err := s.DB.Model(&models.APIKey{}).Where("status = ?", models.KeyStatusActive).Count(&activeKeys).Error; err != nil {
+  response.ErrorI18nFromAPIError(c, app_errors.ErrDatabase, "database.key_count_failed")
+  return
+}
+if err := s.DB.Model(&models.APIKey{}).Where("status = ?", models.KeyStatusInvalid).Count(&invalidKeys).Error; err != nil {
+  response.ErrorI18nFromAPIError(c, app_errors.ErrDatabase, "database.key_count_failed")
+  return
+}

268-319: Minor: reuse a shared weak pattern list to avoid per-call allocs.

Define weakPatterns at package scope.

-func checkPasswordSecurity(c *gin.Context, password, keyType string) []models.SecurityWarning {
+var weakPatterns = []string{
+  "password", "123456", "admin", "secret", "test", "demo",
+  "sk-123456", "key", "token", "pass", "pwd", "qwerty",
+  "abc", "default", "user", "login", "auth", "temp",
+}
+
+func checkPasswordSecurity(c *gin.Context, password, keyType string) []models.SecurityWarning {
   var warnings []models.SecurityWarning
   // ...
-  weakPatterns := []string{
-    "password", "123456", "admin", "secret", "test", "demo",
-    "sk-123456", "key", "token", "pass", "pwd", "qwerty",
-    "abc", "default", "user", "login", "auth", "temp",
-  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ea2d20 and 55d2591.

⛔ Files ignored due to path filters (2)
  • go.sum is excluded by !**/*.sum
  • web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (47)
  • go.mod (2 hunks)
  • internal/app/app.go (2 hunks)
  • internal/handler/dashboard_handler.go (7 hunks)
  • internal/handler/group_handler.go (21 hunks)
  • internal/handler/handler.go (3 hunks)
  • internal/handler/key_handler.go (16 hunks)
  • internal/handler/log_handler.go (2 hunks)
  • internal/handler/settings_handler.go (3 hunks)
  • internal/handler/task_handler.go (1 hunks)
  • internal/i18n/i18n.go (1 hunks)
  • internal/i18n/locales/en-US.go (1 hunks)
  • internal/i18n/locales/ja-JP.go (1 hunks)
  • internal/i18n/locales/zh-CN.go (1 hunks)
  • internal/i18n/middleware.go (1 hunks)
  • internal/response/response.go (3 hunks)
  • internal/router/router.go (2 hunks)
  • internal/types/types.go (1 hunks)
  • web/package.json (1 hunks)
  • web/src/api/keys.ts (2 hunks)
  • web/src/api/logs.ts (2 hunks)
  • web/src/components/AppFooter.vue (7 hunks)
  • web/src/components/BaseInfoCard.vue (6 hunks)
  • web/src/components/GlobalTaskProgressBar.vue (4 hunks)
  • web/src/components/LanguageSelector.vue (1 hunks)
  • web/src/components/Layout.vue (3 hunks)
  • web/src/components/LineChart.vue (6 hunks)
  • web/src/components/Logout.vue (2 hunks)
  • web/src/components/NavBar.vue (2 hunks)
  • web/src/components/SecurityAlert.vue (7 hunks)
  • web/src/components/ThemeToggle.vue (2 hunks)
  • web/src/components/common/ProxyKeysInput.vue (8 hunks)
  • web/src/components/keys/GroupCopyModal.vue (7 hunks)
  • web/src/components/keys/GroupFormModal.vue (27 hunks)
  • web/src/components/keys/GroupInfoCard.vue (21 hunks)
  • web/src/components/keys/GroupList.vue (4 hunks)
  • web/src/components/keys/KeyCreateDialog.vue (5 hunks)
  • web/src/components/keys/KeyDeleteDialog.vue (5 hunks)
  • web/src/components/keys/KeyTable.vue (30 hunks)
  • web/src/components/logs/LogTable.vue (22 hunks)
  • web/src/locales/en-US.ts (1 hunks)
  • web/src/locales/index.ts (1 hunks)
  • web/src/locales/ja-JP.ts (1 hunks)
  • web/src/locales/zh-CN.ts (1 hunks)
  • web/src/main.ts (1 hunks)
  • web/src/utils/http.ts (4 hunks)
  • web/src/views/Login.vue (5 hunks)
  • web/src/views/Settings.vue (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (12)
internal/handler/task_handler.go (2)
internal/response/response.go (1)
  • ErrorI18nFromAPIError (63-69)
internal/errors/errors.go (1)
  • ErrInternalServer (32-32)
internal/handler/handler.go (2)
internal/i18n/middleware.go (2)
  • Message (107-110)
  • Success (57-67)
internal/response/response.go (1)
  • Success (26-33)
internal/handler/log_handler.go (1)
internal/i18n/middleware.go (1)
  • Message (107-110)
internal/handler/settings_handler.go (2)
internal/i18n/middleware.go (1)
  • Message (107-110)
internal/response/response.go (1)
  • SuccessI18n (44-51)
internal/router/router.go (1)
internal/i18n/middleware.go (1)
  • Middleware (16-33)
internal/app/app.go (1)
internal/i18n/i18n.go (1)
  • Init (19-32)
internal/i18n/i18n.go (4)
internal/i18n/middleware.go (1)
  • Message (107-110)
internal/i18n/locales/en-US.go (1)
  • MessagesEnUS (4-173)
internal/i18n/locales/ja-JP.go (1)
  • MessagesJaJP (4-173)
internal/i18n/locales/zh-CN.go (1)
  • MessagesZhCN (4-173)
internal/handler/group_handler.go (4)
internal/response/response.go (3)
  • ErrorI18nFromAPIError (63-69)
  • Error (36-41)
  • SuccessI18n (44-51)
internal/errors/errors.go (4)
  • ErrValidation (29-29)
  • ErrInternalServer (32-32)
  • ErrBadRequest (27-27)
  • ErrDatabase (33-33)
internal/channel/factory.go (1)
  • GetChannels (33-39)
internal/i18n/middleware.go (2)
  • Error (83-92)
  • Message (107-110)
internal/response/response.go (2)
internal/i18n/middleware.go (1)
  • Message (107-110)
internal/errors/errors.go (1)
  • APIError (14-18)
internal/handler/key_handler.go (3)
internal/response/response.go (2)
  • ErrorI18nFromAPIError (63-69)
  • SuccessI18n (44-51)
internal/errors/errors.go (3)
  • ErrBadRequest (27-27)
  • ErrValidation (29-29)
  • ErrResourceNotFound (31-31)
internal/services/key_service.go (1)
  • KeyService (44-49)
internal/handler/dashboard_handler.go (4)
internal/response/response.go (1)
  • ErrorI18nFromAPIError (63-69)
internal/errors/errors.go (1)
  • ErrDatabase (33-33)
internal/models/types.go (3)
  • DashboardStatsResponse (137-143)
  • StatCard (120-126)
  • SecurityWarning (129-134)
internal/i18n/middleware.go (1)
  • Message (107-110)
internal/i18n/middleware.go (2)
internal/i18n/i18n.go (2)
  • GetLocalizer (49-59)
  • T (113-129)
internal/response/response.go (2)
  • Success (26-33)
  • Error (36-41)
🔇 Additional comments (51)
web/package.json (1)

39-39: vue-i18n dependency — LGTM

Version ^9.14.5 is compatible with Vue 3.5. No issues.

web/src/components/BaseInfoCard.vue (1)

6-9: i18n wiring — LGTM

Importing useI18n and using t(...) looks correct.

go.mod (3)

16-22: i18n deps — LGTM

Adding go-i18n/v2 and x/text aligns with the backend i18n usage.


61-61: Indirect x/sync update — LGTM

No compatibility issues expected.


5-5: CI auto-downloads Go 1.24.3
Verified go version automatically fetches and installs Go 1.24.3 in the CI environment—no changes needed.

internal/i18n/locales/zh-CN.go (1)

48-63: Template placeholders corrected to go-i18n style — LGTM

Using {{.var}} ensures interpolation works with go-i18n v2.

web/src/components/common/ProxyKeysInput.vue (1)

18-19: i18n integration — LGTM

All user-facing strings now use t(...).

web/src/locales/zh-CN.ts (1)

1-486: Locale coverage — LGTM

Keys used in updated components (e.g., dashboard.totalKeys, keys.*) are present.

internal/types/types.go (1)

22-26: All i18n keys from internal/types/types.go are defined in both back-end and front-end locale bundles.

internal/router/router.go (1)

86-88: Good: i18n middleware applied to API group.

This scopes localization to API responses without affecting static assets.

web/src/components/Logout.vue (1)

5-8: LGTM: i18n usage is correct.

Key usage aligns with the new plugin.

Also applies to: 23-24

web/src/main.ts (1)

4-4: No changes needed: locales export a Composition API–compatible i18n instance
web/src/locales/index.ts defines const i18n = createI18n({ legacy: false, … }) on lines 45–48, satisfying the Composition API requirement.

internal/handler/handler.go (2)

86-91: LGTM: localized validation error.


98-106: LGTM: localized auth messages with constant-time compare.

internal/handler/task_handler.go (1)

14-15: LGTM: error now localized via i18n helper.

web/src/api/logs.ts (1)

1-1: LGTM: localized missing-auth message.

internal/handler/settings_handler.go (2)

5-5: LGTM on i18n import.

Importing the i18n package here is appropriate for localized responses.


88-88: Verify existence of i18n key “settings.update_success” across all locales.

Ensure zh-CN, en-US, and ja-JP define this key; otherwise clients may see the raw key or an empty message depending on fallback behavior.

web/src/api/keys.ts (2)

1-1: LGTM on i18n bootstrap import.

Using the central i18n instance here is appropriate for API-layer messaging.


195-195: Confirm i18n is initialized before API usage and align other API modules.

Ensure "@/locales" exports an initialized instance so i18n.global.t is available at runtime. Consider updating similar “no auth” messages in sibling APIs for consistency.

web/src/components/Layout.vue (1)

4-4: LGTM: Language selector integration (desktop + mobile).

Placement and conditional rendering look good; no behavioral concerns.

Also applies to: 41-41, 57-57

internal/app/app.go (2)

13-13: LGTM on importing i18n.


79-84: Initialize i18n early — good; please confirm middleware wiring order.

Ensure router attaches the i18n middleware before API routes so handlers get a localizer via context. Also confirm i18n.Init() is idempotent if Start() can be invoked multiple times in tests.

web/src/components/keys/GroupList.vue (1)

7-11: LGTM: Solid i18n pass on placeholders, empty state, and CTA.

No functional risk; translations are parameter-free and safe here.

Also applies to: 82-87, 98-101, 137-137

web/src/components/ThemeToggle.vue (1)

6-9: LGTM: i18n-ized tooltips and guidance.

The switch hints are correctly localized and parameterized.

Also applies to: 16-36, 51-54

web/src/components/keys/KeyDeleteDialog.vue (1)

24-25: i18n integration looks correct.

Using useI18n and t() is consistent with the app’s i18n approach.

web/src/components/keys/KeyCreateDialog.vue (1)

24-25: i18n usage and success toast are consistent.

Also applies to: 61-61

web/src/components/LineChart.vue (2)

332-333: Locale change won’t update “All Groups” option unless reloaded.

options is populated once in fetchGroups; if locale switches without a reload, the label won’t react. If LanguageSelector always reloads, you’re fine; otherwise, recompute the first option when locale changes.

Option (no fetch needed):

// Add in setup:
import { useI18n } from "vue-i18n";
const { t, locale } = useI18n();
watch(locale, () => {
  const first = { label: t("charts.allGroups"), value: null };
  if (groupOptions.value.length) groupOptions.value[0] = first;
});

Also applies to: 381-381


376-377: Titles and loading text localized — good.

Also applies to: 561-562

web/src/components/LanguageSelector.vue (1)

11-16: Options derived from SUPPORTED_LOCALES — LGTM.

web/src/utils/http.ts (1)

42-45: Localized toasts for success and error — looks good.

Also applies to: 56-65, 66-69

web/src/views/Settings.vue (2)

24-25: i18n adoption in form labels, validation, and button states — solid.

Also applies to: 40-41, 188-189


137-147: Placeholders localized appropriately — good.

Also applies to: 154-165

web/src/components/keys/GroupCopyModal.vue (2)

75-79: Good: localized error when source group is missing.


121-123: Title composition with i18n and display name — good.

web/src/components/GlobalTaskProgressBar.vue (1)

7-10: LGTM: i18n wiring

Nice, minimal integration of vue-i18n with a single t() source.

web/src/components/keys/KeyTable.vue (1)

204-206: LGTM: localized error message

Good switch to i18n for the “load keys failed” path.

web/src/views/Login.vue (1)

3-7: LGTM: i18n + LanguageSelector

Clean integration; placeholders and button text now localized.

web/src/components/AppFooter.vue (1)

133-209: LGTM: links + labels localized

Footer links and tooltips are properly driven by i18n.

web/src/components/SecurityAlert.vue (1)

14-17: LGTM: i18n applied to header, buttons, severity, and summaries

Good coverage; logic unchanged.

internal/response/response.go (3)

25-33: LGTM: Success now localized

Defaults to “common.success”; structure unchanged.


43-51: LGTM: i18n helpers are straightforward

Signatures and varargs for template data look good.


35-41: Verify i18n middleware is applied to your API routes
Confirm that i18n.Middleware is used on the router group (or individual routes) serving these handlers, since i18n.Message requires a localizer in context.

internal/handler/key_handler.go (2)

137-150: LGTM: query param validation + localized errors

Good early returns and clear error keys.


365-407: LGTM: success messages localized with counts

Nice use of SuccessI18n for bulk operations.

internal/i18n/middleware.go (2)

15-33: LGTM: middleware correctly seeds Localizer and lang per request


1-111: Verified no endpoints use removed i18n response helpers
Ripgrep search returned zero references to i18n.Success* or i18n.Error*, so it’s safe to remove them.

web/src/components/keys/GroupInfoCard.vue (2)

26-31: Good: i18n wired correctly.

Using useI18n and centralizing text via t(...) is the right move here.


348-445: Ensure Naive UI components are registered
NDivider, NStatistic, and NGradientText are used in the template but not explicitly imported. Confirm that unplugin-vue-components with NaiveUiResolver is enabled in your Vite config, or add these imports:

 import {
   NButton,
   NButtonGroup,
   NCard,
+  NDivider,
+  NStatistic,
+  NGradientText,
   NCollapse,
   NCollapseItem,
   NForm,
   NFormItem,
   NGrid,
   NGridItem,
   NIcon,
   NInput,
   NSpin,
   NTag,
   NTooltip,
   useDialog,
 } from "naive-ui";
internal/handler/group_handler.go (1)

679-688: Nice touch: translating config option metadata dynamically.

This keeps option labels consistent with the selected locale.

internal/handler/dashboard_handler.go (1)

91-109: Good: i18n applied to dashboard labels and datasets.

SubValueTip and dataset labels now respect locale.

Also applies to: 158-175

Comment on lines 59 to 61
log.Printf("Failed to stream log keys to CSV: %v", err)
c.JSON(500, gin.H{"error": "Failed to export logs"})
c.JSON(500, gin.H{"error": i18n.Message(c, "error.export_logs")})
return
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Don’t mix CSV stream with JSON error; conditionally abort without changing content type after write.

Currently, if streaming fails mid-response, sending JSON corrupts the download.

-    log.Printf("Failed to stream log keys to CSV: %v", err)
-    c.JSON(500, gin.H{"error": i18n.Message(c, "error.export_logs")})
-    return
+    logrus.WithError(err).Error("Failed to stream log keys to CSV")
+    // If nothing has been written yet, return a JSON error; otherwise just abort.
+    if !c.Writer.Written() {
+      c.AbortWithStatusJSON(500, gin.H{"error": i18n.Message(c, "error.export_logs")})
+    } else {
+      c.Abort()
+    }
+    return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
log.Printf("Failed to stream log keys to CSV: %v", err)
c.JSON(500, gin.H{"error": "Failed to export logs"})
c.JSON(500, gin.H{"error": i18n.Message(c, "error.export_logs")})
return
logrus.WithError(err).Error("Failed to stream log keys to CSV")
// If nothing has been written yet, return a JSON error; otherwise just abort.
if !c.Writer.Written() {
c.AbortWithStatusJSON(500, gin.H{"error": i18n.Message(c, "error.export_logs")})
} else {
c.Abort()
}
return
🤖 Prompt for AI Agents
In internal/handler/log_handler.go around lines 59 to 61, the handler currently
writes a CSV stream then, on error, writes a JSON error which corrupts the
download; change the error path to first check whether the response has already
been started (use c.Writer.Written() or response size/status) and if the
response has been written simply log the error and return without writing JSON
or modifying headers, otherwise send the JSON error as before; ensure you do not
alter content-type or write additional bytes after the CSV stream has begun.

Comment on lines +56 to +104
// Success 返回成功响应(带国际化消息)
func Success(c *gin.Context, msgID string, data any) {
localizer := GetLocalizerFromContext(c)
message := T(localizer, msgID)

c.JSON(200, gin.H{
"success": true,
"message": message,
"data": data,
"lang": GetLangFromContext(c),
})
}

// SuccessWithData 返回成功响应(带模板数据)
func SuccessWithData(c *gin.Context, msgID string, templateData map[string]any, data any) {
localizer := GetLocalizerFromContext(c)
message := T(localizer, msgID, templateData)

c.JSON(200, gin.H{
"success": true,
"message": message,
"data": data,
"lang": GetLangFromContext(c),
})
}

// Error 返回错误响应(带国际化消息)
func Error(c *gin.Context, code int, msgID string) {
localizer := GetLocalizerFromContext(c)
message := T(localizer, msgID)

c.JSON(code, gin.H{
"success": false,
"message": message,
"lang": GetLangFromContext(c),
})
}

// ErrorWithData 返回错误响应(带模板数据)
func ErrorWithData(c *gin.Context, code int, msgID string, templateData map[string]any) {
localizer := GetLocalizerFromContext(c)
message := T(localizer, msgID, templateData)

c.JSON(code, gin.H{
"success": false,
"message": message,
"lang": GetLangFromContext(c),
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Avoid introducing a second response schema

These helpers return {success:boolean} while existing APIs return {code:number} (see internal/response/response.go). Mixing schemas will break clients that expect code.

Option A (recommended): remove these helpers and keep only Message, using the existing response.Success/Error functions everywhere.

-// Success 返回成功响应(带国际化消息)
-func Success(c *gin.Context, msgID string, data any) {
-  ...
-}
-
-// SuccessWithData 返回成功响应(带模板数据)
-func SuccessWithData(c *gin.Context, msgID string, templateData map[string]any, data any) {
-  ...
-}
-
-// Error 返回错误响应(带国际化消息)
-func Error(c *gin.Context, code int, msgID string) {
-  ...
-}
-
-// ErrorWithData 返回错误响应(带模板数据)
-func ErrorWithData(c *gin.Context, code int, msgID string, templateData map[string]any) {
-  ...
-}

Option B: refactor these helpers to delegate to internal/response so the JSON shape stays consistent.

Would you like me to push a follow-up PR to unify call sites?

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In internal/i18n/middleware.go around lines 56-104 the
Success/SuccessWithData/Error/ErrorWithData helpers emit responses with a
different JSON shape ({success:boolean}) than the project standard
({code:number}) in internal/response/response.go; update these helpers to
preserve the canonical schema by delegating to the internal/response package
(call the existing response.Success/response.Error or response.Message helpers
and pass the localized message and data) or remove these helpers entirely and
replace all call sites to use the existing internal/response functions; ensure
all call sites are updated so clients still receive {code:number,
message:string, ...} and run tests to validate no schema regressions.

@@ -12,6 +12,9 @@ import {
} from "@vicons/ionicons5";
import { NIcon, NTooltip } from "naive-ui";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify NDivider availability

Template uses , but it isn’t imported here. If auto-import (unplugin-vue-components) isn’t configured, add NDivider to imports.


🏁 Script executed:

#!/bin/bash
# Check if unplugin-vue-components is configured to auto-import Naive UI
rg -nP "unplugin-vue-components|NaiveUiResolver" -C2 -- web | sed -n '1,200p'
# Also check for a global component registration of NDivider
rg -nP "\bNDivider\b" -C2 -- web

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Locate Vite config files
fd -t f -I vite.config.* || true

# Print first 200 lines of each Vite config
for cfg in $(fd -t f -I vite.config.*); do
  echo "=== $cfg ==="
  sed -n '1,200p' "$cfg"
done

# Search for unplugin-vue-components references
rg -nP "unplugin-vue-components" -C3 .

# Search for Components(...) plugin usage and NaiveUiResolver
rg -nP "Components\(" -C3 .
rg -nP "NaiveUiResolver" -C3 .

# Check package.json for unplugin-vue-components dependency
grep -R "\"unplugin-vue-components\"" -n package.json || true

# Search for global registration of NDivider in main entry points
rg -nP "component\(\s*['\"]NDivider['\"]" -C3 .
rg -nP "use\(\s*Naive?UI" -C3 .

Length of output: 798


Import missing NDivider in AppFooter.vue
Add NDivider to the import from naive-ui in web/src/components/AppFooter.vue:

import { NIcon, NTooltip, NDivider } from 'naive-ui';
🤖 Prompt for AI Agents
In web/src/components/AppFooter.vue around line 13, the import from 'naive-ui'
is missing NDivider; update the import statement to include NDivider (i.e., add
NDivider to the named imports alongside NIcon and NTooltip) so the component can
use the divider component without undefined import errors.

Comment on lines 574 to 575
{{ value || "-" }}
</n-form-item>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don't hide valid falsy values (0/false) in config display.

value || "-" masks 0 and false. Use nullish coalescing.

-                    {{ value || "-" }}
+                    {{ value ?? "-" }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{{ value || "-" }}
</n-form-item>
{{ value ?? "-" }}
</n-form-item>
🤖 Prompt for AI Agents
In web/src/components/keys/GroupInfoCard.vue around lines 574-575, the template
uses "value || '-'" which hides valid falsy values like 0 and false; replace
this with a nullish check (e.g., use the nullish coalescing operator: value ??
"-" or an explicit (value === null || value === undefined) ? "-" : value) so
only null/undefined render as "-" while preserving 0 and false.

<n-card
style="width: 800px"
:title="`删除 ${groupName || '当前分组'} 的密钥`"
:title="t('keys.deleteKeysFromGroup', { group: groupName || t('keys.currentGroup') })"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify locale keys exist across all locales.

To avoid runtime fallbacks, confirm all keys used here exist in en-US, zh-CN, ja-JP.

Also applies to: 91-91, 98-101, 62-62


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cd web/src/locales
files=$(fd -a 'en-US.ts|zh-CN.ts|ja-JP.ts' .)
keys=(
  'keys.deleteTaskStarted'
  'keys.deleteKeysFromGroup'
  'keys.currentGroup'
  'keys.enterKeysToDeletePlaceholder'
  'common.cancel'
  'common.delete'
)
missing=0
for f in $files; do
  for k in "${keys[@]}"; do
    rg -nP "\b$(printf '%s' "$k" | sed 's/\./\\./g')\b" "$f" >/dev/null || { echo "Missing $k in $f"; missing=1; }
  done
done
exit $missing

Length of output: 1373


Add missing translation keys to all locales: The following keys are absent in web/src/locales/en-US.ts, zh-CN.ts, and ja-JP.ts and must be defined to prevent runtime fallbacks:
• keys.deleteTaskStarted
• keys.deleteKeysFromGroup
• keys.currentGroup
• keys.enterKeysToDeletePlaceholder
• common.cancel
• common.delete

🤖 Prompt for AI Agents
In web/src/components/keys/KeyDeleteDialog.vue around line 74, the template
references translation keys that are missing from web/src/locales/en-US.ts,
zh-CN.ts, and ja-JP.ts; open each of those locale files and add the following
keys under the appropriate namespaces: keys.deleteTaskStarted,
keys.deleteKeysFromGroup, keys.currentGroup, keys.enterKeysToDeletePlaceholder,
and common.cancel and common.delete, providing proper localized strings for each
language; ensure the keys are added in the same object structure as other
translations and exported so they are available at runtime, then run the app or
translation linter to confirm there are no fallback/missing-key warnings.

Comment on lines +30 to 33
import { useI18n } from "vue-i18n";

const { t } = useI18n();

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove zh-CN hardcode from datetime formatting

toLocaleString("zh-CN") breaks i18n. Use current locale from i18n.

-import { useI18n } from "vue-i18n";
+import { useI18n } from "vue-i18n";
@@
-const { t } = useI18n();
+const { t, locale } = useI18n();
@@
-const formatDateTime = (timestamp: string) => {
+const formatDateTime = (timestamp: string) => {
   if (!timestamp) {
     return "-";
   }
   const date = new Date(timestamp);
-  return date.toLocaleString("zh-CN", { hour12: false }).replace(/\//g, "-");
+  try {
+    return new Intl.DateTimeFormat(locale.value, {
+      year: "numeric",
+      month: "2-digit",
+      day: "2-digit",
+      hour: "2-digit",
+      minute: "2-digit",
+      second: "2-digit",
+      hour12: false,
+    }).format(date);
+  } catch {
+    return date.toISOString().replace("T", " ").slice(0, 19);
+  }
 };

Also applies to: 119-125

🤖 Prompt for AI Agents
In web/src/components/logs/LogTable.vue around lines 30-33 and 119-125, remove
the hardcoded toLocaleString("zh-CN") usage and instead use the current locale
from vue-i18n; call useI18n() inside setup (or use the existing import) and read
locale.value (or fallback to navigator.language || 'en-US') and pass that
variable into toLocaleString so the datetime formatting follows the app locale;
ensure you use the reactive locale.value (or a computed) so format updates when
the i18n locale changes and replace both occurrences on the indicated lines.

@tbphp tbphp merged commit 162cfb7 into dev Sep 9, 2025
1 check passed
@tbphp tbphp deleted the feat-internationalization branch September 9, 2025 10:00
@tbphp tbphp added this to the v1.1.1 milestone Sep 9, 2025
@coderabbitai coderabbitai bot mentioned this pull request Sep 9, 2025
5 tasks
@tbphp tbphp modified the milestones: v1.1.1, v1.2.0 Sep 10, 2025
@coderabbitai coderabbitai bot mentioned this pull request Sep 10, 2025
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-language support

1 participant