-
-
Notifications
You must be signed in to change notification settings - Fork 594
feat: Internationalization #252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughAdds 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~70 minutes Possibly related PRs
Pre-merge checks (3 passed, 2 warnings)❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Poem
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.
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 unit tests
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. Comment |
There was a problem hiding this 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_ratestats?.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-CNUse 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 headerKeep table headers fully translated.
- { - title: "Key", + { + title: t("logs.key"), key: "key_value", width: 200,
609-616: Localize “User Agent” label and copy textUse 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 changeStatic 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.textis 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 ensurecomputedis 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 typingThis 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 usageYou 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 responsesAdd 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
menuOptionsonce; depending on vue-i18n reactivity, labels may not refresh on locale change. Prefer passing the i18n key and callingt(...)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
renderMenuItemaccordingly (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
!keysTextwhich 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
Normalizeresponse.config.methodto lowercase and require a non-emptyresponse.data.messagebefore 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) setshideMessage: 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 orderParse 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 IDsReturning 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 headerUse 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 consistencyAvoid 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 fallbacksOptional: 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 → isDeletingMinor 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 resultsWhen 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
⛔ Files ignored due to path filters (2)
go.sumis excluded by!**/*.sumweb/package-lock.jsonis 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 correctuseI18n and t() usage align with the new locale keys.
web/src/locales/en-US.ts (1)
98-110: LGTM: dashboard keys cover new component usageKeys totalKeys, rpm10Min, requests24h, errorRate24h exist and match component references.
go.mod (2)
5-5: Ensure CI runners support Go 1.24.x
The newtoolchain go1.24.3directive 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, andgo list -m allconfirms those exact versions are in use. No unintended upgrades detected.web/src/api/keys.ts (1)
1-1: Good: error message localizedImporting i18n and using auth.noAuthKeyFound aligns with the new i18n setup.
web/src/main.ts (1)
4-9: LGTM: i18n plugin registrationPlugin order is fine; app will have translations available globally.
web/src/api/logs.ts (1)
1-21: Good: export error localizedConsistent 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/i18nloads 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/i18nfor 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.
| // 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 | ||
| } |
There was a problem hiding this comment.
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.
| // 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...) | ||
| } |
There was a problem hiding this comment.
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.
| // 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).
| <div class="stat-value"> | ||
| {{ stats?.rpm?.value.toFixed(1) ?? 0 }} | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| <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).
| 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" }, | ||
| ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| // 更多操作下拉菜单选项 | ||
| 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" }, | ||
| ]; |
There was a problem hiding this comment.
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.
| // 更多操作下拉菜单选项 | |
| 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.
| 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" }, | ||
| ]; |
There was a problem hiding this comment.
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.
| 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.
| onePerLine: "One per line", | ||
| separated: "Separator separated", | ||
| keyPlaceholder: "Enter keys, one per line", | ||
| noGroups: "No groups", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| // 获取默认语言 | ||
| 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"; | ||
| } |
There was a problem hiding this comment.
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.
| // 获取默认语言 | |
| 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.
| "キー検証完了、{total}個のキーを処理、{valid}個成功、{invalid}個失敗。注意:検証失敗でもすぐにブラックリストに追加されるわけではありません。失敗回数が闾値に達する必要があります。", | ||
| importCompleted: "キーインポート完了、{added}個追加、{ignored}個無視。", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| "キー検証完了、{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.
There was a problem hiding this 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 typeWith 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 failsRight 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 translationsLabels 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 piecemealConcatenation 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” labelRendering 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 overwritesMultiple 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.gointernal/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
📒 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 wiringUsing vue-i18n’s t() across UI strings is consistent and correct.
Also applies to: 30-31
170-173: Nice fallback copyFalling back to a localized “no description” keeps the UI coherent.
276-279: LGTM on copy URL messagingGood reuse of i18n for success/failure toasts.
321-326: Button tooltips/titles localized correctlyEdit/Delete/Copy titles wired to t(); looks good.
Also applies to: 336-337
82-85: LGTM on localized copy feedbackGood success/error messaging via t().
451-451: Good: section title localizedDetail section header now uses t(); consistent with the i18n effort.
296-304: Header i18n and copy-to-clipboard UX look goodSelectGroup fallback and tooltip copy hint are localized correctly.
1-290: Verification: ensure all i18n keys exist across localesAutomate 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.") PYinternal/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 -C2internal/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{}) stringexists and uses a Localizer derived from the request:#!/bin/bash rg -nP 'func\s+Message\s*\(\s*\*gin\.Context' internal/i18n -C2internal/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 supportsfor 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
| // 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 | ||
| } |
There was a problem hiding this comment.
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.
| // 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 | |
| } |
internal/i18n/locales/en-US.go
Outdated
| "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}", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
internal/i18n/locales/zh-CN.go
Outdated
| "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": "测试模型不能为空或只有空格", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this 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 valueAvoid 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 formatterChain 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 handleUse 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 insteadstatusConfig 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 undefinedCalling 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}} — resolvedPrevious 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 localeOtherwise 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 GetLocalizerAvoid 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 changesUse 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 modeGuard
localStorage/navigatoraccess 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 chainingThe 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” stringsYou have both "success": "操作成功" and "common.success": "成功". Consider aligning "success" to "成功" for consistency with other locales.
- "success": "操作成功", + "success": "成功",
95-103: Optional native review for toneSome 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 randomnessMath.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 keyConcatenating “危险操作” + “下的所有密钥…” 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 absentPass 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 → isDeletingKeeps 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 i18nReturn 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.PrintfStay 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 changeColumn headers won’t update after a locale switch because
columnsis built once. Wrap incomputed.-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.$messageis brittle and can break SSR/tests. You already havemessage = 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 truthThree 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 neededAlso applies to: 209-233, 235-261
335-344: Add error handling for async option fetchesSurface 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
GetLangFromContextreturns normalizedAccept-Language, which may differ from the actual fallback chosen byGetLocalizer. Consider derivinglangfrom the resolved localizer or storing the normalized, supported value fromnormalizeLanguageCode(...).
56-56: Optional: use http.Status constants instead of magic numbers*If you keep JSON helpers, prefer
http.StatusOKetc.web/src/locales/index.ts (2)
61-74: Avoid full page reload on locale changeSwitch 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/SEOSet 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
⛔ Files ignored due to path filters (2)
go.sumis excluded by!**/*.sumweb/package-lock.jsonis 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 — LGTMVersion ^9.14.5 is compatible with Vue 3.5. No issues.
web/src/components/BaseInfoCard.vue (1)
6-9: i18n wiring — LGTMImporting useI18n and using t(...) looks correct.
go.mod (3)
16-22: i18n deps — LGTMAdding go-i18n/v2 and x/text aligns with the backend i18n usage.
61-61: Indirect x/sync update — LGTMNo compatibility issues expected.
5-5: CI auto-downloads Go 1.24.3
Verifiedgo versionautomatically 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 — LGTMUsing {{.var}} ensures interpolation works with go-i18n v2.
web/src/components/common/ProxyKeysInput.vue (1)
18-19: i18n integration — LGTMAll user-facing strings now use t(...).
web/src/locales/zh-CN.ts (1)
1-486: Locale coverage — LGTMKeys 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 definesconst 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 wiringNice, minimal integration of vue-i18n with a single t() source.
web/src/components/keys/KeyTable.vue (1)
204-206: LGTM: localized error messageGood switch to i18n for the “load keys failed” path.
web/src/views/Login.vue (1)
3-7: LGTM: i18n + LanguageSelectorClean integration; placeholders and button text now localized.
web/src/components/AppFooter.vue (1)
133-209: LGTM: links + labels localizedFooter links and tooltips are properly driven by i18n.
web/src/components/SecurityAlert.vue (1)
14-17: LGTM: i18n applied to header, buttons, severity, and summariesGood coverage; logic unchanged.
internal/response/response.go (3)
25-33: LGTM: Success now localizedDefaults to “common.success”; structure unchanged.
43-51: LGTM: i18n helpers are straightforwardSignatures and varargs for template data look good.
35-41: Verify i18n middleware is applied to your API routes
Confirm thati18n.Middlewareis used on the router group (or individual routes) serving these handlers, sincei18n.Messagerequires a localizer in context.internal/handler/key_handler.go (2)
137-150: LGTM: query param validation + localized errorsGood early returns and clear error keys.
365-407: LGTM: success messages localized with countsNice 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 toi18n.Success*ori18n.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
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| // 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), | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
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"; | |||
There was a problem hiding this comment.
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 -- webLength 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.
| {{ value || "-" }} | ||
| </n-form-item> |
There was a problem hiding this comment.
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.
| {{ 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') })" |
There was a problem hiding this comment.
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 $missingLength 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.
| import { useI18n } from "vue-i18n"; | ||
|
|
||
| const { t } = useI18n(); | ||
|
|
There was a problem hiding this comment.
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.
关联 Issue / Related Issue
Closes #78
变更内容 / Change Content
自查清单 / Checklist
Summary by CodeRabbit
New Features
Improvements
Chores