Просмотр исходного кода

chore: remove outdated translation and implementation guide files

- Deleted several files related to translation guides and checklists, including `IMPLEMENTATION_GUIDE.md`, `TRANSLATION_KEYS_REFERENCE.md`, and `TRANSLATIONS_SUMMARY.md`.
- Updated `.gitignore` to include new translation checklist files.
- Refactored translation files to improve organization and maintainability.
ding113 3 месяцев назад
Родитель
Сommit
daf956d355
71 измененных файлов с 3931 добавлено и 2376 удалено
  1. 4 0
      .gitignore
  2. 0 391
      IMPLEMENTATION_GUIDE.md
  3. 0 329
      TRANSLATIONS_SUMMARY.md
  4. 0 406
      TRANSLATION_KEYS_REFERENCE.md
  5. 0 364
      docs/i18n-workflow.md
  6. 0 66
      messages/TRANSLATIONS_CHECKLIST.txt
  7. 70 2
      messages/en/dashboard.json
  8. 2 0
      messages/en/index.ts
  9. 83 0
      messages/en/internal.json
  10. 159 9
      messages/en/quota.json
  11. 409 28
      messages/en/settings.json
  12. 19 1
      messages/en/usage.json
  13. 2 0
      messages/ja/index.ts
  14. 83 0
      messages/ja/internal.json
  15. 177 27
      messages/ja/quota.json
  16. 115 9
      messages/ja/settings.json
  17. 19 1
      messages/ja/usage.json
  18. 516 0
      messages/providers-i18n-additions.json
  19. 2 0
      messages/ru/index.ts
  20. 83 0
      messages/ru/internal.json
  21. 180 30
      messages/ru/quota.json
  22. 115 9
      messages/ru/settings.json
  23. 19 1
      messages/ru/usage.json
  24. 215 20
      messages/zh-CN/dashboard.json
  25. 2 0
      messages/zh-CN/index.ts
  26. 83 0
      messages/zh-CN/internal.json
  27. 155 5
      messages/zh-CN/quota.json
  28. 327 43
      messages/zh-CN/settings.json
  29. 19 1
      messages/zh-CN/usage.json
  30. 2 0
      messages/zh-TW/index.ts
  31. 83 0
      messages/zh-TW/internal.json
  32. 171 21
      messages/zh-TW/quota.json
  33. 115 9
      messages/zh-TW/settings.json
  34. 19 1
      messages/zh-TW/usage.json
  35. 14 11
      src/app/[locale]/dashboard/_components/dashboard-header.tsx
  36. 1 2
      src/app/[locale]/dashboard/_components/dashboard-nav.tsx
  37. 19 17
      src/app/[locale]/dashboard/_components/statistics/chart.tsx
  38. 14 9
      src/app/[locale]/dashboard/_components/statistics/wrapper.tsx
  39. 3 1
      src/app/[locale]/dashboard/_components/user-menu.tsx
  40. 3 0
      src/app/[locale]/dashboard/layout.tsx
  41. 5 2
      src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx
  42. 20 18
      src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
  43. 11 9
      src/app/[locale]/dashboard/leaderboard/page.tsx
  44. 6 1
      src/app/[locale]/dashboard/logs/page.tsx
  45. 12 2
      src/app/[locale]/dashboard/page.tsx
  46. 4 2
      src/app/[locale]/dashboard/quotas/keys/page.tsx
  47. 9 5
      src/app/[locale]/dashboard/quotas/layout.tsx
  48. 7 1
      src/app/[locale]/dashboard/quotas/page.tsx
  49. 14 13
      src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx
  50. 3 1
      src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-manager.tsx
  51. 4 2
      src/app/[locale]/dashboard/quotas/providers/page.tsx
  52. 9 8
      src/app/[locale]/dashboard/quotas/users/_components/users-quota-client.tsx
  53. 9 7
      src/app/[locale]/dashboard/quotas/users/page.tsx
  54. 62 60
      src/app/[locale]/internal/data-gen/_components/data-generator-page.tsx
  55. 8 1
      src/app/[locale]/internal/data-gen/page.tsx
  56. 6 0
      src/app/[locale]/login/page.tsx
  57. 7 1
      src/app/[locale]/page.tsx
  58. 1 2
      src/app/[locale]/settings/_components/settings-nav.tsx
  59. 15 13
      src/app/[locale]/settings/client-versions/_components/client-version-stats-table.tsx
  60. 15 13
      src/app/[locale]/settings/client-versions/_components/client-version-toggle.tsx
  61. 8 1
      src/app/[locale]/settings/client-versions/page.tsx
  62. 5 3
      src/app/[locale]/settings/config/_components/auto-cleanup-form.tsx
  63. 3 0
      src/app/[locale]/settings/layout.tsx
  64. 7 1
      src/app/[locale]/settings/page.tsx
  65. 14 8
      src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx
  66. 20 18
      src/app/[locale]/settings/sensitive-words/_components/add-word-dialog.tsx
  67. 19 15
      src/app/[locale]/settings/sensitive-words/_components/edit-word-dialog.tsx
  68. 11 5
      src/app/[locale]/settings/sensitive-words/_components/refresh-cache-button.tsx
  69. 30 16
      src/app/[locale]/settings/sensitive-words/_components/word-list-table.tsx
  70. 283 334
      src/app/[locale]/usage-doc/page.tsx
  71. 2 1
      src/components/ui/language-switcher.tsx

+ 4 - 0
.gitignore

@@ -59,3 +59,7 @@ next-env.d.ts
 
 # dev tools (local development utilities)
 /dev/
+
+# translation checklists and temporary docs
+TRANSLATIONS_CHECKLIST.txt
+TRANSLATIONS_CHECKLIST.md

+ 0 - 391
IMPLEMENTATION_GUIDE.md

@@ -1,391 +0,0 @@
-# UI Component Translation Implementation Guide
-
-## Quick Start
-
-### Step 1: Use Translations in Components
-
-```typescript
-// src/components/ui/data-table.tsx
-import { useTranslations } from "next-intl";
-
-export function DataTable<T extends TableData>({ ... }) {
-  const t = useTranslations("ui");
-
-  if (data.length === 0) {
-    return <p>{t("common.noData")}</p>;
-  }
-}
-```
-
-### Step 2: Verify Translation Keys
-
-All translation keys are documented in `TRANSLATION_KEYS_REFERENCE.md`.
-
-### Step 3: Test with Multiple Locales
-
-The application automatically loads translations based on the active locale:
-
-- English: `/en/*`
-- Japanese: `/ja/*`
-- Simplified Chinese: `/zh-CN/*`
-- Traditional Chinese: `/zh-TW/*`
-- Russian: `/ru/*`
-
-## Integration Checklist
-
-- [x] Created `ui.json` for all 5 locales
-  - UI states, table operations, error messages, pagination
-- [x] Created `common.json` for all 5 locales
-  - Action buttons, shared terms, status indicators
-
-- [x] Created `forms.json` for all 5 locales
-  - Form validation, messages, placeholders, errors
-
-- [x] Validated all JSON files (100% valid)
-
-- [x] Created reference documentation
-  - `TRANSLATION_KEYS_REFERENCE.md`
-  - `TRANSLATIONS_SUMMARY.md`
-
-## File Locations
-
-```
-messages/
-├── en/
-│   ├── ui.json ............... 47 lines
-│   ├── common.json ........... 43 lines
-│   └── forms.json ............ 51 lines
-├── ja/
-│   ├── ui.json ............... 47 lines
-│   ├── common.json ........... 43 lines
-│   └── forms.json ............ 51 lines
-├── ru/
-│   ├── ui.json ............... 47 lines
-│   ├── common.json ........... 43 lines
-│   └── forms.json ............ 51 lines
-├── zh-CN/
-│   ├── ui.json ............... 47 lines
-│   ├── common.json ........... 43 lines
-│   └── forms.json ............ 51 lines
-└── zh-TW/
-    ├── ui.json ............... 47 lines
-    ├── common.json ........... 43 lines
-    └── forms.json ............ 51 lines
-```
-
-## Components Using These Translations
-
-### 1. DataTable Component
-
-- **File**: `src/components/ui/data-table.tsx`
-- **Keys Used**:
-  - `ui.common.noData` (line 107)
-  - `ui.common.actions` (line 280)
-
-### 2. List Component
-
-- **File**: `src/components/ui/list.tsx`
-- **Keys Used**:
-  - `ui.common.noData` (line 240)
-
-### 3. Error Boundary
-
-- **File**: `src/components/error-boundary.tsx`
-- **Keys Used**:
-  - `ui.errorBoundary.title`
-  - `ui.errorBoundary.defaultDescription`
-  - `ui.errorBoundary.refreshPage`
-  - `ui.errorBoundary.listErrorTitle`
-  - `ui.errorBoundary.listErrorDescription`
-  - `common.retry`
-
-### 4. Form Error Boundary
-
-- **File**: `src/components/form-error-boundary.tsx`
-- **Keys Used**:
-  - `forms.errors.formErrorTitle`
-  - `forms.errors.formErrorDescription`
-  - `ui.common.retry` (through useTranslations("ui"))
-
-### 5. Form Layout
-
-- **File**: `src/components/form/form-layout.tsx`
-- **Keys Used**:
-  - `forms.common.cancel`
-  - `forms.common.processing`
-
-## Key Translation Statistics
-
-| Metric                          | Count   |
-| ------------------------------- | ------- |
-| Total Locales                   | 5       |
-| Total Namespaces                | 3       |
-| Total Files                     | 15      |
-| Keys per Locale                 | 141     |
-| Total Keys (across all locales) | 705     |
-| Average File Size               | ~1.5 KB |
-
-## Namespace Organization
-
-### `ui.json` (47 keys per locale)
-
-Purpose: UI component states and operations
-
-- Common states (noData, loading, empty, error)
-- Table operations (pagination, sorting, filtering)
-- Error boundary messages
-- Pagination controls
-- Empty/Loading state messages
-
-### `common.json` (43 keys per locale)
-
-Purpose: Cross-module shared terminology
-
-- Action buttons (save, cancel, delete, edit, create, etc.)
-- Generic operations (search, filter, export, import, etc.)
-- Status indicators (success, error, warning, info)
-- Time references (today, yesterday, thisWeek, etc.)
-- Boolean states (yes, no, ok)
-
-### `forms.json` (51 keys per locale)
-
-Purpose: Form-specific texts
-
-- Form actions (submit, reset)
-- Form attributes (required, optional, processing)
-- Validation error messages
-- Input placeholders
-- Form-specific messages
-
-## Usage Examples
-
-### Example 1: DataTable with No Data
-
-```typescript
-import { useTranslations } from "next-intl";
-
-export function DataTable({ data, loading, error }) {
-  const t = useTranslations("ui");
-
-  if (loading) {
-    return <p>{t("loading.description")}</p>;
-  }
-
-  if (error) {
-    return <p>{t("errorBoundary.defaultDescription")}</p>;
-  }
-
-  if (data.length === 0) {
-    return <p>{t("common.noData")}</p>;
-  }
-
-  return <table>{/* table content */}</table>;
-}
-```
-
-### Example 2: Form with Validation
-
-```typescript
-import { useTranslations } from "next-intl";
-
-export function LoginForm() {
-  const t = useTranslations("forms");
-  const tCommon = useTranslations("common");
-
-  return (
-    <form>
-      <input
-        type="email"
-        placeholder={t("placeholder.email")}
-        required
-      />
-      {emailError && <span>{t("errors.email")}</span>}
-
-      <input
-        type="password"
-        placeholder={t("placeholder.password")}
-        required
-      />
-
-      <button type="submit">{tCommon("submit")}</button>
-      <button type="button" onClick={onCancel}>
-        {tCommon("cancel")}
-      </button>
-    </form>
-  );
-}
-```
-
-### Example 3: Error Boundary
-
-```typescript
-import { useTranslations } from "next-intl";
-import { ErrorBoundary } from "@/components/error-boundary";
-
-function AppComponent() {
-  const t = useTranslations("ui");
-
-  return (
-    <ErrorBoundary
-      fallback={({ error, resetError }) => (
-        <div>
-          <h3>{t("errorBoundary.title")}</h3>
-          <p>{error?.message || t("errorBoundary.defaultDescription")}</p>
-          <button onClick={resetError}>
-            {t("common.retry")}
-          </button>
-        </div>
-      )}
-    >
-      {/* Your component */}
-    </ErrorBoundary>
-  );
-}
-```
-
-## Testing Translations
-
-### Manual Testing
-
-1. **Switch Locale**: Change your locale in the app settings
-2. **Verify Text**: Check that all UI text matches the selected locale
-3. **Check Placeholders**: Ensure dynamic values ({min}, {max}, etc.) are replaced correctly
-
-### Automated Testing
-
-```typescript
-// Example test
-import { render, screen } from "@testing-library/react";
-import { NextIntlClientProvider } from "next-intl";
-import DataTable from "@/components/ui/data-table";
-
-const messages = {
-  ui: {
-    common: { noData: "No data available" }
-  }
-};
-
-test("displays no data message", () => {
-  render(
-    <NextIntlClientProvider locale="en" messages={messages}>
-      <DataTable data={[]} />
-    </NextIntlClientProvider>
-  );
-
-  expect(screen.getByText("No data available")).toBeInTheDocument();
-});
-```
-
-## Maintenance Guide
-
-### Adding New Translation Keys
-
-1. Identify the appropriate namespace:
-   - `ui.json` - UI states and component operations
-   - `common.json` - Shared generic terms
-   - `forms.json` - Form-specific texts
-
-2. Add the key to all 5 locale files:
-
-   ```json
-   {
-     "ui": {
-       "newSection": {
-         "newKey": "English translation"
-       }
-     }
-   }
-   ```
-
-3. Update documentation:
-   - Add to `TRANSLATION_KEYS_REFERENCE.md`
-   - Update `TRANSLATIONS_SUMMARY.md`
-
-4. Verify JSON validity:
-   ```bash
-   jq empty messages/*/ui.json  # Check if valid
-   ```
-
-### Updating Translations
-
-1. Locate the translation in the appropriate file
-2. Update all 5 locale files simultaneously
-3. Maintain consistent structure and formatting
-4. Re-validate JSON files
-5. Test with multiple locales
-
-### Handling Placeholders
-
-Translations support dynamic content using placeholders:
-
-```json
-{
-  "errors": {
-    "minLength": "Minimum length is {min} characters",
-    "maxLength": "Maximum length is {max} characters"
-  }
-}
-```
-
-Usage:
-
-```typescript
-const t = useTranslations("forms");
-const message = t("errors.minLength", { min: 5 });
-// Output: "Minimum length is 5 characters"
-```
-
-## Deployment Checklist
-
-- [x] All 15 translation files created
-- [x] All JSON files validated
-- [x] Keys aligned across all locales
-- [x] Reference documentation created
-- [x] Implementation guide provided
-- [x] File ownership clearly defined
-- [x] Ready for production use
-
-## Troubleshooting
-
-### Missing Translation Key
-
-**Problem**: Component shows key name instead of translation (e.g., "ui.common.noData")
-
-**Solution**:
-
-1. Verify the key exists in all locale files
-2. Check spelling and nesting
-3. Use `TRANSLATION_KEYS_REFERENCE.md` to find correct key
-
-### Inconsistent Translations
-
-**Problem**: Same component shows different text in different locales
-
-**Solution**:
-
-1. Check that all locale files have identical key structure
-2. Verify no typos in key names
-3. Ensure all placeholders are consistent
-
-### JSON Validation Error
-
-**Problem**: Translation file won't load
-
-**Solution**:
-
-1. Validate JSON: `jq empty messages/en/ui.json`
-2. Check for syntax errors (missing commas, quotes)
-3. Use JSON formatter to identify issues
-
-## Support Resources
-
-- **Reference Guide**: `TRANSLATION_KEYS_REFERENCE.md`
-- **Summary**: `TRANSLATIONS_SUMMARY.md`
-- **Component Examples**: See components using translations in source
-- **Documentation**: See individual locale file comments
-
----
-
-**Version**: 1.0
-**Created**: 2024
-**Status**: Production Ready

+ 0 - 329
TRANSLATIONS_SUMMARY.md

@@ -1,329 +0,0 @@
-# UI Component Translations Summary
-
-## Overview
-
-This document provides a comprehensive summary of the translations created for shared UI components across 5 locales: English (en), Japanese (ja), Russian (ru), Simplified Chinese (zh-CN), and Traditional Chinese (zh-TW).
-
-## Translation Files Created
-
-All translation files follow the `messages/{locale}/{namespace}.json` structure:
-
-### File Structure
-
-```
-messages/
-├── en/
-│   ├── ui.json         # UI component texts (47 lines)
-│   ├── common.json     # Cross-module shared terms (43 lines)
-│   └── forms.json      # Form-related texts (51 lines)
-├── ja/
-│   ├── ui.json
-│   ├── common.json
-│   └── forms.json
-├── ru/
-│   ├── ui.json
-│   ├── common.json
-│   └── forms.json
-├── zh-CN/
-│   ├── ui.json
-│   ├── common.json
-│   └── forms.json
-└── zh-TW/
-    ├── ui.json
-    ├── common.json
-    └── forms.json
-```
-
-**Total**: 15 translation files, 141 lines each locale (705 total lines)
-
-## Namespaces
-
-### 1. ui.json - UI Component Texts
-
-Used for displaying UI states and table/list component texts.
-
-**Key Categories:**
-
-- `common` - Basic UI states (noData, actions, loading, empty, error)
-- `table` - Table-specific features (pagination, sorting, filtering, search, refresh, columns)
-- `errorBoundary` - Error messages for error boundaries (title, description, refresh page, list errors)
-- `pagination` - Pagination controls (first, last, previous, next, page navigation)
-- `empty` - Empty state messages
-- `loading` - Loading state messages
-
-**Translation Keys** (Sample):
-
-```json
-{
-  "common": {
-    "noData": "No data available",
-    "actions": "Actions",
-    "loading": "Loading...",
-    "empty": "Empty",
-    "error": "Error"
-  },
-  "errorBoundary": {
-    "title": "Something went wrong",
-    "listErrorTitle": "Failed to load list",
-    "listErrorDescription": "An error occurred while loading the list..."
-  }
-}
-```
-
-### 2. common.json - Cross-Module Shared Terms
-
-Used for generic action buttons and common terminology shared across all modules.
-
-**Key Categories:**
-
-- Action buttons (save, cancel, delete, confirm, edit, create, close, back, next, previous, retry)
-- Generic operations (refresh, search, filter, export, import, submit, reset, view, copy, download, upload, add, remove, apply, clear)
-- Boolean/UI states (ok, yes, no)
-- Time references (today, yesterday, thisWeek, thisMonth, thisYear)
-- Status indicators (loading, error, success, warning, info, noData, emptyState)
-
-**Translation Keys** (Sample):
-
-```json
-{
-  "save": "Save",
-  "cancel": "Cancel",
-  "delete": "Delete",
-  "retry": "Retry",
-  "loading": "Loading...",
-  "success": "Success",
-  "noData": "No data"
-}
-```
-
-### 3. forms.json - Form-Related Texts
-
-Used for form components, validation messages, and form-specific UI.
-
-**Key Categories:**
-
-- Form actions (submit, reset)
-- Form attributes (required, optional, processing)
-- Common form texts (cancel, processing)
-- Error messages (formErrorTitle, formErrorDescription, validation errors)
-- Input placeholders (text, number, email, password, search, select, date, textarea)
-- Validation messages (required, invalid, length constraints, format validation)
-- Form messages (success, error, saved, deleted, loading)
-
-**Translation Keys** (Sample):
-
-```json
-{
-  "submit": "Submit",
-  "reset": "Reset",
-  "errors": {
-    "formErrorTitle": "Form error",
-    "required": "This field is required",
-    "email": "Please enter a valid email address"
-  },
-  "placeholder": {
-    "text": "Enter text",
-    "email": "Enter your email"
-  }
-}
-```
-
-## Component Integration
-
-### Components Using These Translations
-
-**ui.json:**
-
-- `src/components/ui/data-table.tsx` - Line 62, 107
-- `src/components/ui/list.tsx` - Line 195, 240
-- `src/components/error-boundary.tsx` - Line 57, 64, 66, 75, 86, 93, 96, 100
-- `src/components/form-error-boundary.tsx` - Line 14
-
-**common.json:**
-
-- `src/components/error-boundary.tsx` - Line 72, 100
-- `src/components/form/form-layout.tsx` - Line 72, 79, 124, 131
-- `src/components/form-error-boundary.tsx` - Line 28
-
-**forms.json:**
-
-- `src/components/form/form-layout.tsx` - Line 51, 102
-- `src/components/form-error-boundary.tsx` - Line 13, 21, 24
-
-## Translation Locales
-
-### 1. English (en)
-
-- **File Paths**: `/messages/en/{ui,common,forms}.json`
-- **Language Code**: `en`
-- **Status**: Complete
-- **Key Features**:
-  - Standard English terminology
-  - Formal tone suitable for professional application
-  - Complete coverage of all keys
-
-### 2. Japanese (ja)
-
-- **File Paths**: `/messages/ja/{ui,common,forms}.json`
-- **Language Code**: `ja`
-- **Status**: Complete
-- **Key Features**:
-  - Katakana for technical terms (e.g., "フォーム" for form)
-  - Polite Japanese tone
-  - Complete coverage including form placeholders
-
-### 3. Russian (ru)
-
-- **File Paths**: `/messages/ru/{ui,common,forms}.json`
-- **Language Code**: `ru`
-- **Status**: Complete
-- **Key Features**:
-  - Full Russian translations
-  - Appropriate formal tone
-  - Complete coverage of all categories
-
-### 4. Simplified Chinese (zh-CN)
-
-- **File Paths**: `/messages/zh-CN/{ui,common,forms}.json`
-- **Language Code**: `zh-CN`
-- **Status**: Complete
-- **Key Features**:
-  - Mainland China standard Chinese
-  - Common terminology alignment with technical community
-  - Complete coverage including form validation messages
-
-### 5. Traditional Chinese (zh-TW)
-
-- **File Paths**: `/messages/zh-TW/{ui,common,forms}.json`
-- **Language Code**: `zh-TW`
-- **Status**: Complete
-- **Key Features**:
-  - Taiwan/Hong Kong traditional Chinese characters
-  - Appropriate terminology for regional users
-  - Complete coverage with Traditional Chinese variants
-
-## Usage Example
-
-```typescript
-// In React components using next-intl
-import { useTranslations } from "next-intl";
-
-export function DataTable() {
-  const t = useTranslations("ui");
-
-  return (
-    <div>
-      {data.length === 0 && (
-        <p>{t("common.noData")}</p>  // "No data available"
-      )}
-      {error && (
-        <p>{error}</p>
-      )}
-      {loading && (
-        <div>{t("loading.description")}</div>  // "Please wait..."
-      )}
-    </div>
-  );
-}
-```
-
-## Key Mapping Reference
-
-### UI Namespace Keys Used in Components
-
-```
-ui.common.noData         -> "No data available" / "暂无数据" / etc.
-ui.errorBoundary.title   -> "Something went wrong" / "出现错误" / etc.
-ui.errorBoundary.defaultDescription
-ui.errorBoundary.refreshPage
-ui.errorBoundary.listErrorTitle
-ui.errorBoundary.listErrorDescription
-ui.loading.description   -> "Please wait..." / "请稍候..." / etc.
-```
-
-### Common Namespace Keys Used in Components
-
-```
-common.retry             -> "Retry" / "重试" / etc.
-common.cancel            -> "Cancel" / "取消" / etc.
-common.processing        -> "Processing..." / "处理中..." / etc.
-```
-
-### Forms Namespace Keys Used in Components
-
-```
-forms.common.cancel      -> "Cancel" / "取消" / etc.
-forms.common.processing  -> "Processing..." / "处理中..." / etc.
-forms.errors.formErrorTitle
-forms.errors.formErrorDescription
-```
-
-## Translation Coverage
-
-### Completeness Metrics
-
-- **Total Keys**: 141 keys across all 3 namespaces per locale
-- **Locales**: 5 complete locales
-- **Coverage**: 100% - all defined keys translated
-- **Validation**: All JSON files are valid and properly formatted
-
-### Distribution by Namespace
-
-| Namespace   | Key Count | Purpose                                          |
-| ----------- | --------- | ------------------------------------------------ |
-| ui.json     | 47        | UI states, table/list operations, error messages |
-| common.json | 43        | Generic action buttons and shared terms          |
-| forms.json  | 51        | Form validation, messages, placeholders          |
-| **Total**   | **141**   | **Comprehensive UI translation set**             |
-
-## File Ownership
-
-These translation files are **EXCLUSIVE** to the shared UI component system:
-
-- `messages/*/ui.json` - Owned by UI component translations task
-- `messages/*/common.json` - Owned by UI component translations task
-- `messages/*/forms.json` - Owned by UI component translations task
-
-**No other agent or task should modify these files without explicit coordination.**
-
-## Implementation Notes
-
-1. **Namespace Consistency**: All translation keys follow consistent naming patterns (camelCase with dot notation for nesting)
-
-2. **Placeholder Support**: Forms namespace includes `{min}`, `{max}`, `{current}`, `{total}` placeholders for dynamic text
-
-3. **Locale Loading**: The i18n system automatically loads correct locale files based on user selection
-
-4. **Fallback Handling**: If a translation key is missing, the system falls back to the key name itself (e.g., `ui.common.noData`)
-
-5. **JSON Validation**: All files are valid JSON with no syntax errors
-
-## Next Steps
-
-1. **Component Updates**: Update existing components to use these translation keys instead of hardcoded strings
-2. **Testing**: Verify all components render correctly with each locale
-3. **Documentation**: Document any special translation needs for specific components
-4. **Maintenance**: Keep translations updated when new UI states or form validations are added
-
-## Locale Performance Considerations
-
-- **File Size**: Each locale JSON file is approximately 1-2 KB
-- **Load Time**: Negligible impact on page load (files are static and cacheable)
-- **Bundle Impact**: Translations are loaded separately from JavaScript bundle
-
-## Export Statistics
-
-```
-Total Locales:     5 (en, ja, ru, zh-CN, zh-TW)
-Total Namespaces:  3 (ui, common, forms)
-Total Files:       15
-Total Keys:        705 (141 keys × 5 locales)
-Total Lines:       705 (consistent across all locales)
-Validation Status: All files valid JSON
-```
-
----
-
-**Created**: 2024
-**Status**: Complete and Ready for Use
-**Version**: 1.0

+ 0 - 406
TRANSLATION_KEYS_REFERENCE.md

@@ -1,406 +0,0 @@
-# Translation Keys Reference Guide
-
-Quick lookup guide for all translation keys available for UI components.
-
-## How to Use Translations
-
-```typescript
-// In your React component
-import { useTranslations } from "next-intl";
-
-export function MyComponent() {
-  const t = useTranslations("ui");           // For ui.json keys
-  const tCommon = useTranslations("common");  // For common.json keys
-  const tForms = useTranslations("forms");    // For forms.json keys
-
-  return (
-    <div>
-      <p>{t("common.noData")}</p>  // "No data available"
-      <button>{tCommon("save")}</button>  // "Save"
-      <input placeholder={tForms("placeholder.text")} />  // "Enter text"
-    </div>
-  );
-}
-```
-
-## UI Namespace (`ui.json`)
-
-### Common UI States
-
-```
-ui.common.noData        "No data available"
-ui.common.actions       "Actions"
-ui.common.loading       "Loading..."
-ui.common.empty         "Empty"
-ui.common.error         "Error"
-```
-
-### Table Operations
-
-```
-ui.table.pagination     "Pagination"
-ui.table.sorting        "Sort"
-ui.table.filtering      "Filter"
-ui.table.search         "Search"
-ui.table.refresh        "Refresh"
-ui.table.columns        "Columns"
-ui.table.previousPage   "Previous"
-ui.table.nextPage       "Next"
-ui.table.pageInfo       "Page {current} of {total}"
-ui.table.itemsPerPage   "Items per page"
-ui.table.selectAll      "Select all"
-ui.table.deselectAll    "Deselect all"
-```
-
-### Error Boundary
-
-```
-ui.errorBoundary.title                  "Something went wrong"
-ui.errorBoundary.defaultDescription     "An unexpected error occurred..."
-ui.errorBoundary.refreshPage            "Refresh page"
-ui.errorBoundary.listErrorTitle         "Failed to load list"
-ui.errorBoundary.listErrorDescription   "An error occurred while loading the list..."
-```
-
-### Pagination
-
-```
-ui.pagination.first     "First"
-ui.pagination.last      "Last"
-ui.pagination.previous  "Previous"
-ui.pagination.next      "Next"
-ui.pagination.goToPage  "Go to page"
-ui.pagination.pageSize  "Page size"
-ui.pagination.total     "Total {total} items"
-```
-
-### Empty & Loading States
-
-```
-ui.empty.title          "No data"
-ui.empty.description    "No data to display"
-
-ui.loading.title        "Loading"
-ui.loading.description  "Please wait..."
-```
-
-## Common Namespace (`common.json`)
-
-### Action Buttons
-
-```
-common.save             "Save"
-common.cancel           "Cancel"
-common.delete           "Delete"
-common.confirm          "Confirm"
-common.edit             "Edit"
-common.create           "Create"
-common.close            "Close"
-common.back             "Back"
-common.next             "Next"
-common.previous         "Previous"
-common.retry            "Retry"
-common.refresh          "Refresh"
-```
-
-### Form Operations
-
-```
-common.search           "Search"
-common.filter           "Filter"
-common.export           "Export"
-common.import           "Import"
-common.submit           "Submit"
-common.reset            "Reset"
-```
-
-### View Operations
-
-```
-common.view             "View"
-common.copy             "Copy"
-common.download         "Download"
-common.upload           "Upload"
-common.add              "Add"
-common.remove           "Remove"
-common.apply            "Apply"
-common.clear            "Clear"
-```
-
-### Boolean States
-
-```
-common.ok               "OK"
-common.yes              "Yes"
-common.no               "No"
-```
-
-### Time References
-
-```
-common.today            "Today"
-common.yesterday        "Yesterday"
-common.thisWeek         "This week"
-common.thisMonth        "This month"
-common.thisYear         "This year"
-```
-
-### Status Indicators
-
-```
-common.loading          "Loading..."
-common.error            "Error"
-common.success          "Success"
-common.warning          "Warning"
-common.info             "Info"
-common.noData           "No data"
-common.emptyState       "No data to display"
-```
-
-## Forms Namespace (`forms.json`)
-
-### Form Actions
-
-```
-forms.submit            "Submit"
-forms.reset             "Reset"
-```
-
-### Form Attributes
-
-```
-forms.required          "Required"
-forms.optional          "Optional"
-forms.processing        "Processing..."
-```
-
-### Common Form Texts
-
-```
-forms.common.cancel         "Cancel"
-forms.common.processing     "Processing..."
-```
-
-### Error Messages
-
-```
-forms.errors.formErrorTitle             "Form error"
-forms.errors.formErrorDescription       "An error occurred while processing the form..."
-
-forms.errors.required                   "This field is required"
-forms.errors.invalid                    "Invalid input"
-forms.errors.minLength                  "Minimum length is {min} characters"
-forms.errors.maxLength                  "Maximum length is {max} characters"
-forms.errors.min                        "Minimum value is {min}"
-forms.errors.max                        "Maximum value is {max}"
-forms.errors.email                      "Please enter a valid email address"
-forms.errors.url                        "Please enter a valid URL"
-forms.errors.pattern                    "Format does not match requirements"
-```
-
-### Input Placeholders
-
-```
-forms.placeholder.text                  "Enter text"
-forms.placeholder.number                "Enter a number"
-forms.placeholder.email                 "Enter your email"
-forms.placeholder.password              "Enter your password"
-forms.placeholder.search                "Search..."
-forms.placeholder.select                "Select an option"
-forms.placeholder.date                  "Select a date"
-forms.placeholder.textarea              "Enter text here"
-```
-
-### Validation Messages
-
-```
-forms.validation.required               "This field is required"
-forms.validation.invalid                "Invalid format"
-forms.validation.tooShort               "Input too short"
-forms.validation.tooLong                "Input too long"
-forms.validation.invalidEmail           "Invalid email format"
-forms.validation.invalidUrl             "Invalid URL format"
-forms.validation.invalidNumber          "Invalid number format"
-```
-
-### Form Messages
-
-```
-forms.messages.success                  "Operation successful"
-forms.messages.error                    "Operation failed"
-forms.messages.saved                    "Saved successfully"
-forms.messages.deleted                  "Deleted successfully"
-forms.messages.loading                  "Processing..."
-forms.messages.submit                   "Submitting..."
-```
-
-## Usage Patterns
-
-### DataTable Component
-
-```typescript
-const t = useTranslations("ui");
-
-// Show no data message
-{data.length === 0 && <p>{t("common.noData")}</p>}
-
-// Loading state
-{loading && <div>{t("loading.description")}</div>}
-
-// Table headers
-<TableHead>{t("table.columns")}</TableHead>
-<Button>{t("table.refresh")}</Button>
-```
-
-### Form Component
-
-```typescript
-const t = useTranslations("forms");
-const tCommon = useTranslations("common");
-
-// Input with placeholder
-<input placeholder={t("placeholder.email")} />
-
-// Validation error
-{error && <span>{t("errors.required")}</span>}
-
-// Submit button
-<Button type="submit">{tCommon("submit")}</Button>
-
-// Cancel button
-<Button onClick={onCancel}>{tCommon("cancel")}</Button>
-```
-
-### Error Boundary
-
-```typescript
-const t = useTranslations("ui");
-
-// Error title
-<h3>{t("errorBoundary.title")}</h3>
-
-// Error description
-<p>{error?.message || t("errorBoundary.defaultDescription")}</p>
-
-// Retry button
-<Button>{t("common.retry")}</Button>
-```
-
-## Localization Notes by Language
-
-### English (en)
-
-- Standard professional English
-- Clear and concise terminology
-- Suitable for international business context
-
-### Japanese (ja)
-
-- Uses appropriate Japanese grammatical structures
-- Technical terms in katakana
-- Polite form suitable for professional interfaces
-
-### Simplified Chinese (zh-CN)
-
-- Mainland China standard terminology
-- Clear and direct communication style
-- Aligns with common technical community usage
-
-### Traditional Chinese (zh-TW)
-
-- Taiwan/Hong Kong character set
-- Regional terminology alignment
-- Professional and respectful tone
-
-### Russian (ru)
-
-- Full Russian language support
-- Proper case and gender agreement
-- Professional tone for business application
-
-## Best Practices
-
-1. **Always Use Translation Keys**: Never hardcode UI strings
-
-   ```typescript
-   // Good
-   <p>{t("common.noData")}</p>
-
-   // Avoid
-   <p>No data available</p>
-   ```
-
-2. **Use Correct Namespace**: Choose the right namespace for context
-
-   ```typescript
-   // Use common for generic actions
-   const tCommon = useTranslations("common");
-
-   // Use forms for form-specific texts
-   const tForms = useTranslations("forms");
-
-   // Use ui for UI component states
-   const tUI = useTranslations("ui");
-   ```
-
-3. **Handle Missing Keys Gracefully**: The i18n system will fallback to key name
-
-   ```typescript
-   // If key is missing, displays: "common.save"
-   {
-     tCommon("save");
-   }
-   ```
-
-4. **Use Placeholders for Dynamic Content**: Format strings with placeholders
-
-   ```typescript
-   // Template has: "Minimum length is {min} characters"
-   {
-     t("errors.minLength", { min: 5 });
-   } // "Minimum length is 5 characters"
-   ```
-
-5. **Keep Keys Consistent**: Use the provided keys, don't create new ones
-   - This ensures consistency across all locales
-   - Prevents translation maintenance issues
-   - Makes code reviews easier
-
-## File Locations
-
-```
-/messages/en/ui.json          English UI texts
-/messages/en/common.json      English common terms
-/messages/en/forms.json       English form texts
-
-/messages/ja/ui.json          Japanese UI texts
-/messages/ja/common.json      Japanese common terms
-/messages/ja/forms.json       Japanese form texts
-
-/messages/ru/ui.json          Russian UI texts
-/messages/ru/common.json      Russian common terms
-/messages/ru/forms.json       Russian form texts
-
-/messages/zh-CN/ui.json       Simplified Chinese UI texts
-/messages/zh-CN/common.json   Simplified Chinese common terms
-/messages/zh-CN/forms.json    Simplified Chinese form texts
-
-/messages/zh-TW/ui.json       Traditional Chinese UI texts
-/messages/zh-TW/common.json   Traditional Chinese common terms
-/messages/zh-TW/forms.json    Traditional Chinese form texts
-```
-
-## Support & Maintenance
-
-For questions or to request new translation keys:
-
-1. Check this reference guide first
-2. Verify the key exists in all 5 locales
-3. Ensure consistent usage across components
-4. Document any custom keys added to the codebase
-
----
-
-**Last Updated**: 2024
-**Version**: 1.0
-**Maintenance**: Exclusive to UI component translations task

+ 0 - 364
docs/i18n-workflow.md

@@ -1,364 +0,0 @@
-# Translation Management Workflow
-
-This document describes the translation workflow, key naming conventions, and how to add new translations for developers.
-
-## Translation File Structure
-
-```
-messages/
-├── zh-CN/          # Chinese Simplified (简体中文) - Default locale
-│   ├── common.json
-│   ├── auth.json
-│   ├── dashboard.json
-│   ├── settings.json
-│   └── usage.json
-├── zh-TW/          # Chinese Traditional (繁体中文)
-│   └── ...
-├── en/             # English
-│   └── ...
-├── ru/             # Russian (Русский)
-│   └── ...
-└── ja/             # Japanese (日本語)
-    └── ...
-```
-
-## Translation Key Naming Convention
-
-Translation keys follow a hierarchical structure: `namespace.section.key`
-
-### Namespaces
-
-- **common**: Shared UI elements (navigation, actions, status, time, validation)
-- **auth**: Authentication related (login, logout, security warnings)
-- **dashboard**: Dashboard pages (overview, logs, leaderboard, sessions, quotas)
-- **settings**: Settings pages (providers, prices, config, data, notifications, etc.)
-- **usage**: Usage documentation page
-
-### Sections
-
-Within each namespace, organize keys into logical sections:
-
-- `nav`: Navigation items
-- `actions`: Action buttons/links (save, cancel, delete, edit, etc.)
-- `title`: Page/section titles
-- `description`: Descriptive text
-- `labels`: Form labels
-- `placeholders`: Input placeholders
-- `errors`: Error messages
-- `toasts`: Toast notifications
-- `dialogs`: Dialog content
-- `columns`: Table columns
-
-### Examples
-
-```json
-{
-  "common": {
-    "nav": {
-      "dashboard": "仪表盘",
-      "settings": "设置"
-    },
-    "actions": {
-      "save": "保存",
-      "cancel": "取消"
-    }
-  },
-  "dashboard": {
-    "overview": {
-      "title": "概览",
-      "totalRequests": "总请求数"
-    }
-  }
-}
-```
-
-## Using Translations in Components
-
-### Client Components
-
-Use the `useTranslations()` hook:
-
-```tsx
-"use client";
-
-import { useTranslations } from "next-intl";
-
-export function MyComponent() {
-  const t = useTranslations("namespace");
-
-  return (
-    <div>
-      <h1>{t("title.pageTitle")}</h1>
-      <p>{t("description.text")}</p>
-      <button>{t("actions.save")}</button>
-    </div>
-  );
-}
-```
-
-### Server Components
-
-Use the `getTranslations()` function:
-
-```tsx
-import { getTranslations } from "next-intl/server";
-
-export default async function MyPage() {
-  const t = await getTranslations("namespace");
-
-  return (
-    <div>
-      <h1>{t("title.pageTitle")}</h1>
-      <p>{t("description.text")}</p>
-    </div>
-  );
-}
-```
-
-### Dynamic Values
-
-Use variables in translations:
-
-```json
-{
-  "greeting": "你好,{name}!"
-}
-```
-
-```tsx
-const t = useTranslations("common");
-<p>{t("greeting", { name: userName })}</p>;
-```
-
-## Automated String Extraction
-
-Use the extraction script to find and extract hardcoded Chinese strings:
-
-```bash
-# Dry run - preview extraction without modifying files
-npx tsx scripts/extract-translations.ts --dry-run --verbose
-
-# Extract and update translation files
-npx tsx scripts/extract-translations.ts --verbose
-
-# Extract from specific directory
-npx tsx scripts/extract-translations.ts --target src/app/[locale]/dashboard
-```
-
-### Extraction Process
-
-1. **Scan**: Script scans TSX files for Chinese characters (`[\u4e00-\u9fa5]+`)
-2. **Generate Keys**: Auto-generates semantic keys based on context
-3. **Update Files**: Adds new translations to `messages/zh-CN/*.json`
-4. **Review**: Keys marked with `needsReview: true` require manual refinement
-
-### Manual Key Refinement
-
-Auto-generated keys like `key0key1key2` should be manually refined to semantic names:
-
-```json
-// Before (auto-generated)
-{
-  "title": {
-    "key0key1key2key3key4": "消耗排行榜"
-  }
-}
-
-// After (manually refined)
-{
-  "title": {
-    "costRanking": "消耗排行榜"
-  }
-}
-```
-
-## Adding New Translations
-
-### Step 1: Add Chinese (zh-CN) Translation
-
-Add the new key to the appropriate namespace file in `messages/zh-CN/`:
-
-```json
-{
-  "dashboard": {
-    "newFeature": {
-      "title": "新功能标题",
-      "description": "新功能描述"
-    }
-  }
-}
-```
-
-### Step 2: Add Placeholder for Other Locales
-
-Add the same key structure to other locale files (`zh-TW/`, `en/`, `ru/`, `ja/`):
-
-```json
-{
-  "dashboard": {
-    "newFeature": {
-      "title": "New Feature Title",
-      "description": "New feature description"
-    }
-  }
-}
-```
-
-### Step 3: Use Translation in Code
-
-```tsx
-const t = useTranslations("dashboard");
-<div>
-  <h2>{t("newFeature.title")}</h2>
-  <p>{t("newFeature.description")}</p>
-</div>;
-```
-
-### Step 4: Validate JSON Syntax
-
-Ensure all JSON files are valid:
-
-```bash
-# Check syntax for all translation files
-find messages -name "*.json" -exec node -e "JSON.parse(require('fs').readFileSync('{}', 'utf-8'))" \;
-```
-
-## Translation Workflow
-
-### For Developers
-
-1. **Write new UI**: Use Chinese strings directly in code initially
-2. **Extract strings**: Run extraction script to pull strings into translation files
-3. **Refine keys**: Manually rename auto-generated keys to semantic names
-4. **Use translations**: Replace hardcoded strings with `t()` calls
-5. **Test**: Verify translations render correctly in all locales
-
-### For Translators
-
-1. **Source**: Always use `messages/zh-CN/*.json` as the source of truth
-2. **Translate**: Update corresponding keys in target locale files
-3. **Context**: Check the application UI for context if needed
-4. **Review**: Have a native speaker review translations for accuracy
-
-## TypeScript Type Safety
-
-next-intl provides type-safe translation keys when configured correctly:
-
-```tsx
-// TypeScript will autocomplete and validate translation keys
-const t = useTranslations("dashboard");
-t("overview.title"); // ✓ Valid
-t("invalid.key"); // ✗ TypeScript error
-```
-
-## Common Patterns
-
-### Conditional Rendering
-
-```tsx
-{
-  isLoading ? t("common.status.loading") : t("common.actions.submit");
-}
-```
-
-### Lists/Arrays
-
-```json
-{
-  "weekdays": ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
-}
-```
-
-```tsx
-const t = useTranslations("common");
-const weekdays = t.raw("weekdays") as string[];
-```
-
-### Pluralization
-
-```json
-{
-  "itemCount": "{count, plural, =0 {没有项目} =1 {1 个项目} other {# 个项目}}"
-}
-```
-
-```tsx
-t("itemCount", { count: items.length });
-```
-
-## Best Practices
-
-### Do's
-
-✅ Use semantic, descriptive key names
-✅ Group related translations under common sections
-✅ Keep translation keys in sync across all locales
-✅ Use the extraction script to find hardcoded strings
-✅ Review auto-generated keys and rename them
-✅ Test with different locales before committing
-
-### Don'ts
-
-❌ Don't use auto-generated keys like `key0key1key2` in production
-❌ Don't hardcode strings directly in components
-❌ Don't forget to add keys to all locale files
-❌ Don't nest keys too deeply (max 3-4 levels)
-❌ Don't mix namespaces (keep related content together)
-
-## Troubleshooting
-
-### Translation Not Showing
-
-**Problem**: `t("myKey")` returns the key instead of translation
-**Solution**:
-
-- Verify key exists in `messages/{locale}/{namespace}.json`
-- Check namespace matches `useTranslations("namespace")`
-- Ensure JSON syntax is valid
-
-### TypeScript Error
-
-**Problem**: "Argument of type 'string' is not assignable to parameter"
-**Solution**:
-
-- Add key to translation file
-- Restart TypeScript server
-- Check for typos in key path
-
-### Missing Translation in Non-Default Locale
-
-**Problem**: Fallback to Chinese appears in English locale
-**Solution**:
-
-- Add translation to `messages/en/{namespace}.json`
-- Copy key structure from `messages/zh-CN/`
-- Translate value to target language
-
-## File Organization Reference
-
-```
-src/
-├── app/
-│   └── [locale]/               # Locale-aware pages
-│       ├── layout.tsx          # NextIntlClientProvider setup
-│       ├── page.tsx            # Home page (server component)
-│       ├── login/
-│       │   └── page.tsx        # Client component example
-│       └── dashboard/
-│           └── page.tsx        # Server component example
-├── i18n/
-│   ├── request.ts              # Server-side i18n configuration
-│   └── routing.ts              # Locale-aware routing
-└── messages/                   # Translation files
-    ├── zh-CN/
-    ├── zh-TW/
-    ├── en/
-    ├── ru/
-    └── ja/
-```
-
-## Further Reading
-
-- [next-intl Documentation](https://next-intl.dev/)
-- [Next.js App Router i18n Guide](https://next-intl.dev/docs/getting-started/app-router)
-- [Translation Best Practices](https://next-intl.dev/docs/usage/messages)

+ 0 - 66
messages/TRANSLATIONS_CHECKLIST.txt

@@ -1,66 +0,0 @@
-TRANSLATIONS COMPLETENESS CHECKLIST
-====================================
-
-Locale: en (English)
-  ✓ ui.json         (47 lines, 9 keys at level 1)
-  ✓ common.json     (43 lines, 34 keys)
-  ✓ forms.json      (51 lines, 6 top-level keys)
-  ✓ JSON validation passed
-  Status: COMPLETE
-
-Locale: ja (Japanese - 日本語)
-  ✓ ui.json         (47 lines, 9 keys at level 1)
-  ✓ common.json     (43 lines, 34 keys)
-  ✓ forms.json      (51 lines, 6 top-level keys)
-  ✓ JSON validation passed
-  Status: COMPLETE
-
-Locale: ru (Russian - Русский)
-  ✓ ui.json         (47 lines, 9 keys at level 1)
-  ✓ common.json     (43 lines, 34 keys)
-  ✓ forms.json      (51 lines, 6 top-level keys)
-  ✓ JSON validation passed
-  Status: COMPLETE
-
-Locale: zh-CN (Simplified Chinese - 简体中文)
-  ✓ ui.json         (47 lines, 9 keys at level 1)
-  ✓ common.json     (43 lines, 34 keys)
-  ✓ forms.json      (51 lines, 6 top-level keys)
-  ✓ JSON validation passed
-  Status: COMPLETE
-
-Locale: zh-TW (Traditional Chinese - 繁體中文)
-  ✓ ui.json         (47 lines, 9 keys at level 1)
-  ✓ common.json     (43 lines, 34 keys)
-  ✓ forms.json      (51 lines, 6 top-level keys)
-  ✓ JSON validation passed
-  Status: COMPLETE
-
-====================================
-SUMMARY
-====================================
-
-Total Files:           15
-Total Locales:         5
-Total Keys Per Locale: 141
-Total Keys All:        705
-
-Files by Namespace:
-  ui.json:      5 files × 47 lines = 235 lines
-  common.json:  5 files × 43 lines = 215 lines
-  forms.json:   5 files × 51 lines = 255 lines
-  ─────────────────────────────────────
-  TOTAL:                              705 lines
-
-Validation Results:
-  ✓ All 15 files are valid JSON
-  ✓ All files follow consistent structure
-  ✓ All locales have identical key structure
-  ✓ All placeholder syntax is correct ({min}, {max}, {current}, {total})
-
-Component Coverage:
-  ✓ ui.json       - DataTable, List, ErrorBoundary components
-  ✓ common.json   - Generic action buttons, shared terms
-  ✓ forms.json    - Form validation, messages, placeholders
-
-OVERALL STATUS: ✓ COMPLETE AND READY FOR PRODUCTION USE

+ 70 - 2
messages/en/dashboard.json

@@ -74,7 +74,11 @@
     "description": "View cost statistics rankings for users and keys",
     "tabs": {
       "users": "User Rankings",
-      "keys": "Key Rankings"
+      "keys": "Key Rankings",
+      "userRanking": "User Rankings",
+      "providerRanking": "Provider Rankings",
+      "dailyRanking": "Daily Rankings",
+      "monthlyRanking": "Monthly Rankings"
     },
     "columns": {
       "rank": "Rank",
@@ -82,7 +86,30 @@
       "totalCost": "Total Cost",
       "totalRequests": "Total Requests",
       "avgCost": "Average Cost",
-      "lastActive": "Last Active"
+      "lastActive": "Last Active",
+      "user": "User",
+      "requests": "Requests",
+      "tokens": "Tokens",
+      "consumedAmount": "Consumed Amount",
+      "provider": "Provider",
+      "cost": "Cost",
+      "successRate": "Success Rate",
+      "avgResponseTime": "Avg Response Time"
+    },
+    "states": {
+      "loading": "Loading...",
+      "noData": "No data available",
+      "todayNoData": "No data available for today",
+      "monthNoData": "No data available for this month",
+      "fetchFailed": "Failed to fetch leaderboard data"
+    },
+    "permission": {
+      "title": "Permission Required",
+      "restricted": "Access Restricted",
+      "description": "The leaderboard feature requires administrators to enable the \"Allow Global Usage View\" permission.",
+      "adminAction": "Enable this permission.",
+      "userAction": "Please contact an administrator to enable this permission.",
+      "systemSettings": "System Settings"
     }
   },
   "sessions": {
@@ -182,6 +209,47 @@
     "byUsageRate": "By Usage Rate"
   },
   "all": "All",
+  "nav": {
+    "dashboard": "Dashboard",
+    "usageLogs": "Usage Logs",
+    "leaderboard": "Leaderboard",
+    "quotasManagement": "Quotas",
+    "documentation": "Documentation",
+    "systemSettings": "Settings",
+    "feedback": "Feedback",
+    "login": "Login",
+    "logout": "Logout"
+  },
+  "statistics": {
+    "title": "Usage Statistics",
+    "cost": "Cost",
+    "calls": "API Calls",
+    "totalCost": "Total Cost",
+    "totalCalls": "Total API Calls",
+    "timeRange": {
+      "today": "Today",
+      "todayDescription": "Today's usage",
+      "7days": "Last 7 Days",
+      "7daysDescription": "Usage for the last 7 days",
+      "30days": "Last 30 Days",
+      "30daysDescription": "Usage for the last 30 days",
+      "default": "Usage"
+    },
+    "mode": {
+      "keys": "Showing only usage statistics for your keys",
+      "mixed": "Showing your key details and other users' aggregated data",
+      "users": "Showing usage statistics for all users"
+    },
+    "legend": {
+      "selectAll": "Select All",
+      "deselectAll": "Clear",
+      "selected": "Selected"
+    },
+    "states": {
+      "noData": "No statistics data available",
+      "fetchFailed": "Failed to fetch statistics"
+    }
+  },
   "errors": {
     "fetchSystemSettingsFailed": "Failed to fetch system settings",
     "fetchFailed": "Fetch failed",

+ 2 - 0
messages/en/index.ts

@@ -10,6 +10,7 @@ import settings from "./settings.json";
 import ui from "./ui.json";
 import usage from "./usage.json";
 import validation from "./validation.json";
+import internal from "./internal.json";
 
 export default {
   auth,
@@ -24,4 +25,5 @@ export default {
   ui,
   usage,
   validation,
+  internal,
 };

+ 83 - 0
messages/en/internal.json

@@ -0,0 +1,83 @@
+{
+  "dataGenerator": {
+    "tabs": {
+      "usage": "Usage Data Generation",
+      "userBreakdown": "User Usage Breakdown"
+    },
+    "actions": {
+      "reconfigure": "Reconfigure Parameters",
+      "generate": "Generate Data",
+      "exportScreenshot": "Export Screenshot",
+      "exportPDF": "Export PDF"
+    },
+    "params": {
+      "title": "Generation Parameters",
+      "description": "Configure parameters to create simulated log data",
+      "startDate": "Start Time",
+      "endDate": "End Time",
+      "totalCostCny": "Total Amount (CNY)",
+      "totalRecords": "Total Records",
+      "models": "Include Models (comma-separated)",
+      "userIds": "User IDs (comma-separated)",
+      "providerIds": "Provider IDs (comma-separated)",
+      "serviceName": "Service Name",
+      "required": "*",
+      "placeholders": {
+        "totalCostCny": "e.g., 1000",
+        "totalRecords": "e.g., 500 (leave blank to calculate based on amount)",
+        "models": "e.g., claude-3-5-sonnet,gpt-4 (leave blank for all)",
+        "userIds": "e.g., 1,2,3 (leave blank for all)",
+        "providerIds": "e.g., 1,2 (leave blank for all)",
+        "serviceName": "AI Model Inference Service"
+      }
+    },
+    "summary": {
+      "totalRecords": "Total Records",
+      "totalCost": "Total Cost",
+      "totalCostCny": "Total Cost (CNY)",
+      "totalTokens": "Total Tokens",
+      "timeRange": "Time Range",
+      "to": "to",
+      "uniqueUsers": "Total Users",
+      "totalCalls": "Total Calls"
+    },
+    "table": {
+      "usageLogs": {
+        "title": "Usage Logs",
+        "description": "{count} records in total",
+        "columns": {
+          "time": "Time",
+          "user": "User",
+          "key": "Key",
+          "provider": "Provider",
+          "model": "Model",
+          "input": "Input",
+          "output": "Output",
+          "cacheWrite": "Cache Write",
+          "cacheRead": "Cache Read",
+          "cost": "Cost",
+          "duration": "Duration",
+          "status": "Status"
+        }
+      },
+      "userBreakdown": {
+        "title": "User Usage Details",
+        "description": "{count} records in total",
+        "collapseByUser": "Collapse by User",
+        "columns": {
+          "userName": "User Name",
+          "key": "Key",
+          "serviceModel": "Service Model",
+          "totalCalls": "Total Calls",
+          "totalCost": "Total Cost"
+        }
+      }
+    },
+    "status": {
+      "success": "Success"
+    },
+    "errors": {
+      "failed": "Failed to generate logs"
+    }
+  }
+}

+ 159 - 9
messages/en/quota.json

@@ -5,10 +5,10 @@
       "admin": "Admin",
       "user": "User"
     },
-    "keysCountSuffix": "keys",
-    "rpm": "Request Limit",
+    "keysCountSuffix": " keys",
+    "rpm": "Rate Limit",
     "todayCost": "Today's Cost",
-    "exceededNotice": "Quota exceeded, please contact administrator for assistance"
+    "exceededNotice": "Quota exceeded, please contact administrator"
   },
   "countdown": {
     "reset": "Reset Countdown"
@@ -16,23 +16,23 @@
   "windowType": {
     "5h": {
       "label": "5-Hour Rolling",
-      "description": "Tracks spending from the past 5 hours, rolling updates hourly"
+      "description": "Tracks consumption over the past 5 hours, updated hourly"
     },
     "weekly": {
       "label": "Weekly Reset",
-      "description": "Calendar week (resets Monday 00:00), tracks entire week"
+      "description": "Natural week cycle (resets Monday 00:00), weekly statistics"
     },
     "monthly": {
       "label": "Monthly Reset",
-      "description": "Calendar month (resets on 1st of month 00:00), tracks entire month"
+      "description": "Natural month cycle (resets 1st of month 00:00), monthly statistics"
     },
     "daily": {
       "label": "Daily Reset",
-      "description": "Calendar day (resets daily 00:00), tracks by day"
+      "description": "Natural day cycle (resets daily 00:00), daily statistics"
     }
   },
   "toolbar": {
-    "searchPlaceholder": "Search by username or key",
+    "searchPlaceholder": "Search username or key",
     "filter": "Filter",
     "sort": "Sort",
     "refresh": "Refresh",
@@ -51,5 +51,155 @@
       "30s": "30s",
       "60s": "60s"
     }
+  },
+  "layout": {
+    "title": "Quota Management",
+    "description": "View and manage quota usage across all levels",
+    "tabs": {
+      "users": "User Quotas",
+      "keys": "Key Quotas",
+      "providers": "Provider Quotas"
+    }
+  },
+  "users": {
+    "title": "User Quota Statistics",
+    "totalCount": "{count} users total",
+    "noNote": "No note",
+    "rpm": {
+      "label": "RPM Quota",
+      "description": "Requests per minute"
+    },
+    "dailyCost": {
+      "label": "Daily Cost",
+      "resetAt": "Resets at"
+    },
+    "noQuotaData": "Unable to retrieve quota information",
+    "noMatches": "No matching users found",
+    "noData": "No user data available",
+    "sort": {
+      "name": "By Name",
+      "usage": "By Usage"
+    },
+    "filter": {
+      "all": "All",
+      "warning": "Near Limit (>60%)",
+      "exceeded": "Exceeded (≥100%)"
+    }
+  },
+  "providers": {
+    "title": "Provider Quota Statistics",
+    "totalCount": "{count} providers total",
+    "filterCount": "Showing {filtered} / {total} providers",
+    "status": {
+      "enabled": "Enabled",
+      "disabled": "Disabled"
+    },
+    "card": {
+      "priority": "Priority",
+      "weight": "Weight"
+    },
+    "cost5h": {
+      "label": "5-Hour Cost"
+    },
+    "costWeekly": {
+      "label": "Weekly Cost",
+      "resetAt": "Resets at"
+    },
+    "costMonthly": {
+      "label": "Monthly Cost",
+      "resetAt": "Resets at"
+    },
+    "concurrentSessions": {
+      "label": "Concurrent Sessions"
+    },
+    "noQuotaSet": "No quota set",
+    "noQuotaData": "Unable to retrieve quota information",
+    "noMatches": "No matching providers",
+    "unlimitedSection": "Providers without quota ({count})"
+  },
+  "keys": {
+    "title": "Key Quota Statistics",
+    "totalCount": "{users} users, {keys} keys total",
+    "searchPlaceholder": "Search user or key...",
+    "filterLabel": "Filter",
+    "filterCount": "Showing {users} users, {keys} keys",
+    "filter": {
+      "all": "All Keys",
+      "keyQuota": "Key Quota Only",
+      "userQuotaOnly": "User Quota Only",
+      "warning": "Warning (≥60%)",
+      "exceeded": "Exceeded (≥100%)"
+    },
+    "table": {
+      "keyName": "Key Name",
+      "quotaType": "Quota Type",
+      "cost5h": "5-Hour Quota",
+      "costWeekly": "Weekly Quota",
+      "costMonthly": "Monthly Quota",
+      "concurrentSessions": "Concurrent Limit",
+      "status": "Status",
+      "actions": "Actions"
+    },
+    "quotaType": {
+      "independent": "Independent Quota",
+      "inherited": "Inherited from User"
+    },
+    "status": {
+      "disabled": "Disabled",
+      "restricted": "Restricted",
+      "normal": "Normal"
+    },
+    "noMatches": "No matching users or keys",
+    "editDialog": {
+      "title": "Set Key Quota",
+      "description": "Key: {keyName} ({userName})",
+      "cost5h": {
+        "label": "5-Hour Quota (USD)",
+        "placeholder": "Unlimited",
+        "current": "Current usage: {currency}{current} / {currency}{limit}"
+      },
+      "costWeekly": {
+        "label": "Weekly Quota (USD)",
+        "placeholder": "Unlimited",
+        "current": "Current usage: {currency}{current} / {currency}{limit}"
+      },
+      "costMonthly": {
+        "label": "Monthly Quota (USD)",
+        "placeholder": "Unlimited",
+        "current": "Current usage: {currency}{current} / {currency}{limit}"
+      },
+      "concurrentSessions": {
+        "label": "Concurrent Session Quota",
+        "placeholder": "0 = Unlimited",
+        "current": "Current concurrent: {current} / {limit}"
+      },
+      "clearAll": "Clear All Quotas",
+      "save": "Save",
+      "setQuota": "Set Quota",
+      "success": "Quota set successfully",
+      "clearSuccess": "Quota cleared",
+      "error": "Failed to set",
+      "clearError": "Failed to clear",
+      "retryError": "Failed to set, please try again later"
+    },
+    "editUserDialog": {
+      "title": "Set User Quota",
+      "description": "User: {userName}",
+      "rpm": {
+        "label": "Requests Per Minute (RPM)",
+        "placeholder": "60",
+        "current": "Current: {current} / {limit} requests/minute"
+      },
+      "dailyQuota": {
+        "label": "Daily Cost Quota (USD)",
+        "placeholder": "100",
+        "current": "Today's usage: {currency}{current} / {currency}{limit}"
+      },
+      "save": "Save",
+      "editQuota": "Edit Quota",
+      "success": "User quota set successfully",
+      "error": "Failed to set",
+      "retryError": "Failed to set, please try again later"
+    }
   }
-}
+}

+ 409 - 28
messages/en/settings.json

@@ -90,12 +90,12 @@
       "apiKeyPlaceholder": "Enter API key",
       "apiKeyOptional": "Leave empty to keep existing key",
       "weight": "Weight",
-      "weightDesc": "Weighted random probability. Higher weight means higher probability of selection within the same priority.",
+      "weightDesc": "Weighted random probability. Within same priority, higher weight = higher selection probability. E.g. weights 1:2:3 = probabilities 16%:33%:50%",
       "priority": "Priority",
-      "priorityDesc": "Within the same priority, sort by cost multiplier from low to high",
+      "priorityDesc": "Lower number = higher priority (0 is highest). System only selects from highest priority providers. Recommendation: Main=0, Backup=1, Emergency=2",
       "enabled": "Enabled",
       "costMultiplier": "Cost Multiplier",
-      "costMultiplierDesc": "e.g. A (cost 1.0x), C (cost 0.8x)",
+      "costMultiplierDesc": "Cost calculation multiplier. Official=1.0, 20% cheaper=0.8, 20% more expensive=1.2 (up to 4 decimal places)",
       "limitConcurrent": "Concurrent Session Limit",
       "limitConcurrentDesc": "e.g. Provider C has limit of 2, currently 2 active sessions",
       "limitAmount5h": "5-Hour Spending Limit (USD)",
@@ -103,7 +103,7 @@
       "limitAmountWeekly": "Weekly Spending Limit (USD)",
       "limitAmountMonthly": "Monthly Spending Limit (USD)",
       "modelRedirects": "Model Redirects",
-      "modelRedirectsDesc": "Redirect Claude model requests to other provider-supported models",
+      "modelRedirectsDesc": "Redirect Claude Code client model requests (e.g. claude-sonnet-4.5) to upstream provider supported models (e.g. glm-4.6, gemini-pro). For cost optimization or third-party AI integration.",
       "sourceModel": "Source Model Name",
       "sourceModelPlaceholder": "e.g. claude-sonnet-4-5-20250929",
       "sourceModelRequired": "Source model name cannot be empty",
@@ -117,7 +117,7 @@
       "proxyUrl": "Proxy Address",
       "proxyUrlPlaceholder": "e.g. http://proxy.example.com:8080 or socks5://127.0.0.1:1080",
       "proxyFallback": "Proxy Fallback",
-      "proxyFallbackDesc": "Fallback to direct connection when proxy fails",
+      "proxyFallbackDesc": "When enabled, auto try direct connection on proxy failure",
       "testProxy": "Test Connection",
       "testProxySuccess": "Proxy connection successful",
       "testProxyFailed": "Failed to test proxy connection",
@@ -145,7 +145,145 @@
       "searchPlaceholder": "Search provider name, URL, remark...",
       "clearSearch": "Clear search",
       "limit0Means": "0 means unlimited",
-      "leaveEmpty": "Leave empty for unlimited"
+      "leaveEmpty": "Leave empty for unlimited",
+      "providerName": "Provider Name",
+      "providerNameRequired": "Provider Name *",
+      "providerNamePlaceholder": "e.g. Zhipu",
+      "apiAddress": "API Address",
+      "apiAddressRequired": "API Address *",
+      "apiAddressPlaceholder": "e.g. https://open.bigmodel.cn/api/anthropic",
+      "apiKeyRequired": "API Key *",
+      "apiKeyLeaveEmpty": "(Leave empty to keep unchanged)",
+      "apiKeyLeaveEmptyDesc": "Leave empty to keep existing key",
+      "apiKeyCurrent": "Current key:",
+      "websiteUrl": "Provider Website URL",
+      "websiteUrlPlaceholder": "https://example.com",
+      "websiteUrlDesc": "Provider website URL for quick access",
+      "websiteUrlInvalid": "Please enter a valid provider website URL",
+      "expandAll": "Expand All Advanced Configuration",
+      "collapseAll": "Collapse All Advanced Configuration",
+      "routingConfig": "Routing Configuration",
+      "routingConfigSummary": "{models} model whitelist, {redirects} redirects",
+      "routingConfigNone": "Not configured",
+      "providerTypeDesc": "Select the API format type for the provider.",
+      "providerTypeDisabledNote": "Note: Gemini CLI and OpenAI Compatible types are under development",
+      "modelRedirectsLabel": "Model Redirects Configuration",
+      "modelRedirectsOptional": "(Optional)",
+      "modelRedirectsEmpty": "No redirect rules yet. System will auto-rewrite model names after adding rules.",
+      "modelRedirectsCurrentRules": "Current Rules ({count})",
+      "modelRedirectsAddNew": "Add New Rule",
+      "modelRedirectsSourceModel": "User Requested Model",
+      "modelRedirectsSourcePlaceholder": "e.g. claude-sonnet-4-5-20250929",
+      "modelRedirectsTargetModel": "Actual Forwarded Model",
+      "modelRedirectsTargetPlaceholder": "e.g. glm-4.6",
+      "modelRedirectsSourceRequired": "Source model name cannot be empty",
+      "modelRedirectsTargetRequired": "Target model name cannot be empty",
+      "modelRedirectsExists": "Model \"{model}\" already has a redirect rule",
+      "joinClaudePool": "Join Claude Scheduling Pool",
+      "joinClaudePoolDesc": "When enabled, this provider will participate in load balancing with Claude type providers",
+      "joinClaudePoolHelp": "Only available when model redirect config contains mappings to claude-* models. When enabled, this provider will also participate in scheduling when users request claude-* models.",
+      "modelWhitelist": "Model Whitelist",
+      "modelWhitelistDesc": "Limit models this provider can handle. By default, provider can handle all models of its type.",
+      "modelWhitelistLabel": "Allowed Models",
+      "modelWhitelistSelected": "Selected {count} models",
+      "modelWhitelistLoading": "Loading...",
+      "modelWhitelistNotFound": "Model not found",
+      "modelWhitelistSearchPlaceholder": "Search model name...",
+      "modelWhitelistSelectAll": "Select All ({count})",
+      "modelWhitelistClear": "Clear",
+      "modelWhitelistManualAdd": "Manually Add Model",
+      "modelWhitelistManualPlaceholder": "Enter model name (e.g. gpt-5-turbo)",
+      "modelWhitelistManualDesc": "Support adding any model name (not limited to price table)",
+      "modelWhitelistAllowAll": "Allow all {type} models",
+      "modelWhitelistAllowAllClause": "Allow all Claude models",
+      "modelWhitelistAllowAllOpenAI": "Allow all OpenAI models",
+      "modelWhitelistSelectedOnly": "Only allow selected {count} models. Requests for other models won't be routed to this provider.",
+      "scheduleParams": "Scheduling Parameters",
+      "priorityLabel": "Priority",
+      "priorityPlaceholder": "0",
+      "weightLabel": "Weight",
+      "weightPlaceholder": "1",
+      "costMultiplierLabel": "Cost Multiplier",
+      "costMultiplierPlaceholder": "1.0",
+      "providerGroupLabel": "Provider Group",
+      "providerGroupPlaceholder": "e.g. premium, economy",
+      "providerGroupDesc": "Provider group tag. Only users with matching providerGroup can use this provider. Example: Set to \"premium\" to allow only providerGroup=\"premium\" users",
+      "rateLimitConfig": "Rate Limit Configuration",
+      "rateLimitConfigSummary": "5h: ${fiveHour}, Weekly: ${weekly}, Monthly: ${monthly}, Concurrent: {concurrent}",
+      "rateLimitConfigNone": "Unlimited",
+      "limit5hLabel": "5-Hour Spending Limit (USD)",
+      "limitWeeklyLabel": "Weekly Spending Limit (USD)",
+      "limitMonthlyLabel": "Monthly Spending Limit (USD)",
+      "limitConcurrentLabel": "Concurrent Session Limit",
+      "limitPlaceholderUnlimited": "Leave empty for unlimited",
+      "limitPlaceholder0": "0 means unlimited",
+      "circuitBreakerConfig": "Circuit Breaker Configuration",
+      "circuitBreakerConfigSummary": "{failureThreshold} failures / {openDuration} min circuit break / {successThreshold} successes to recover",
+      "circuitBreakerDesc": "Auto circuit break on consecutive failures to avoid overall service quality impact",
+      "failureThreshold": "Failure Threshold (times)",
+      "failureThresholdPlaceholder": "5",
+      "failureThresholdDesc": "How many consecutive failures trigger circuit break",
+      "openDuration": "Circuit Break Duration (minutes)",
+      "openDurationPlaceholder": "30",
+      "openDurationDesc": "How long before auto entering half-open state",
+      "successThreshold": "Recovery Threshold (times)",
+      "successThresholdPlaceholder": "2",
+      "successThresholdDesc": "How many successes in half-open state to fully recover",
+      "proxyConfig": "Proxy Configuration",
+      "proxyConfigSummary": "Proxy configured",
+      "proxyConfigSummaryFallback": " (fallback enabled)",
+      "proxyConfigNone": "Not configured",
+      "proxyConfigDesc": "Configure proxy server to improve provider connectivity (supports HTTP, HTTPS, SOCKS4, SOCKS5)",
+      "proxyAddressLabel": "Proxy Address",
+      "proxyAddressOptional": "(Optional)",
+      "proxyAddressPlaceholder": "e.g. http://proxy.example.com:8080 or socks5://127.0.0.1:1080",
+      "proxyAddressFormats": "Supported formats:",
+      "proxyFallbackLabel": "Fallback to direct on proxy failure",
+      "proxyTestLabel": "Connection Test",
+      "proxyTestButton": "Test Connection",
+      "proxyTestTesting": "Testing...",
+      "proxyTestSuccess": "Connection Successful",
+      "proxyTestFailed": "Connection Failed",
+      "proxyTestDesc": "Test provider URL access via configured proxy (uses HEAD request, no quota consumption)",
+      "proxyTestFillUrl": "Please fill in provider URL first",
+      "proxyTestResultSuccess": "Connection successful {via}",
+      "proxyTestViaProxy": "(via proxy)",
+      "proxyTestViaDirect": "(direct)",
+      "proxyTestResponseTime": "Response time: {time}",
+      "proxyTestStatusCode": "| Status code: {code}",
+      "proxyTestResultFailed": "Connection failed",
+      "proxyTestTimeout": "Connection timeout (5s). Please check:\n1. Is proxy server accessible\n2. Are proxy address and port correct\n3. Are proxy credentials correct",
+      "proxyTestProxyError": "Proxy error: {error}",
+      "proxyTestNetworkError": "Network error: {error}",
+      "proxyTestResultMessage": "{message}",
+      "proxyTestResultStatusCode": "Status code: {code}",
+      "proxyTestResultResponseTime": "Response time: {time}ms",
+      "proxyTestResultConnectionMethod": "Connection method: {via}",
+      "proxyTestResultConnectionMethodProxy": "Proxy",
+      "proxyTestResultConnectionMethodDirect": "Direct",
+      "proxyTestResultErrorType": "Error type: {type}",
+      "codexStrategyConfig": "Codex Instructions Strategy",
+      "codexStrategyConfigAuto": "Auto (Recommended)",
+      "codexStrategyConfigForce": "Force Official",
+      "codexStrategyConfigKeep": "Keep Original",
+      "codexStrategyDesc": "Control how to handle Codex request instructions field, affects upstream gateway compatibility",
+      "codexStrategySelect": "Strategy Selection",
+      "codexStrategyAutoLabel": "Auto (Recommended)",
+      "codexStrategyAutoDesc": "Pass through client instructions, auto retry with official prompt on 400 error",
+      "codexStrategyForceLabel": "Force Official",
+      "codexStrategyForceDesc": "Always use official Codex CLI instructions (~4000+ chars)",
+      "codexStrategyKeepLabel": "Keep Original",
+      "codexStrategyKeepDesc": "Always pass through client instructions, no auto retry (for lenient gateways)",
+      "codexStrategyHint": "Hint: Some strict Codex gateways (e.g. 88code, foxcode) require official instructions. Choose \"Auto\" or \"Force Official\" strategy",
+      "confirmAdd": "Confirm Add",
+      "confirmUpdate": "Confirm Update",
+      "confirmAddPending": "Adding...",
+      "confirmUpdatePending": "Updating...",
+      "deleteButton": "Delete",
+      "validUrlRequired": "Please enter a valid API address",
+      "filterProvider": "Filter by Provider Type",
+      "filterAllProviders": "All Providers",
+      "searchClear": "Clear search"
     },
     "scheduling": "Scheduling Strategy Details",
     "schedulingDesc": "Understand how provider selection works with priority layering, session reuse, load balancing and failover",
@@ -193,32 +331,180 @@
       "circuitBreakerRecovery": "A automatically recovers to half-open after 60 seconds",
       "circuitBreakerRecovery5h": "Auto recovery after 5-hour sliding window",
       "reset": "Manual Circuit Breaker Reset",
-      "resetSuccess": "Circuit breaker reset"
-    }
+      "resetSuccess": "Circuit breaker reset",
+      "title": "Core Principles",
+      "priorityFirst": "1️⃣ Priority First: Select only from highest priority (lowest number) providers",
+      "costOptimize": "2️⃣ Cost Optimization: Within same priority, lower cost multiplier has higher probability",
+      "healthFilter": "3️⃣ Health Filtering: Auto skip circuit-broken or over-limit providers",
+      "sessionReuse": "4️⃣ Session Reuse: Consecutive chats reuse same provider, saving context costs",
+      "scenariosTitle": "Interactive Scenario Demos",
+      "bestPracticesTitle": "Best Practices",
+      "bestPracticesPriority": "• Priority Settings: Core providers=0, Backup=1-3",
+      "bestPracticesWeight": "• Weight Config: Set weight by capacity (higher capacity = higher weight)",
+      "bestPracticesCost": "• Cost Multiplier: Official=1.0, Self-hosted can be 0.8-1.2",
+      "bestPracticesLimit": "• Limit Settings: Set 5h, 7d, 30d limits based on budget",
+      "bestPracticesConcurrent": "• Concurrent Control: Set session concurrency by provider API limits",
+      "scenario1Title": "Priority Layering",
+      "scenario1Desc": "System first filters by priority, selecting only from highest priority providers",
+      "scenario1Step1": "Initial State",
+      "scenario1Step1Desc": "4 enabled providers with different priorities",
+      "scenario1Step1Before": "Provider A (priority 0), B (priority 1), C (priority 0), D (priority 2)",
+      "scenario1Step1After": "Filtered to highest priority (0) providers: A, C",
+      "scenario1Step1Decision": "Select only from A and C, B and D filtered out",
+      "scenario1Step2": "Cost Sorting",
+      "scenario1Step2Desc": "Within same priority, sort by cost multiplier low to high",
+      "scenario1Step2Before": "A (cost 1.0x), C (cost 0.8x)",
+      "scenario1Step2After": "After sorting: C (0.8x), A (1.0x)",
+      "scenario1Step2Decision": "Lower cost C has higher selection probability",
+      "scenario1Step3": "Weighted Random",
+      "scenario1Step3Desc": "Use weight for random selection, higher weight = higher probability",
+      "scenario1Step3Before": "C (weight 3), A (weight 1)",
+      "scenario1Step3After": "C has 75% probability, A has 25%",
+      "scenario1Step3Decision": "Finally randomly selected C",
+      "scenario2Title": "User Group Filtering",
+      "scenario2Desc": "If user has provider group specified, system prioritizes selection from that group",
+      "scenario2Step1": "Check User Group",
+      "scenario2Step1Desc": "User configured providerGroup = 'premium'",
+      "scenario2Step1Before": "All providers: A (default), B (premium), C (premium), D (economy)",
+      "scenario2Step1After": "Filtered to 'premium' group: B, C",
+      "scenario2Step1Decision": "Select only from B and C",
+      "scenario2Step2": "Group Fallback",
+      "scenario2Step2Desc": "If no available providers in user group, fallback to all providers",
+      "scenario2Step2Before": "All providers in user group 'vip' disabled or over limit",
+      "scenario2Step2After": "Fallback to all enabled providers: A, B, C, D",
+      "scenario2Step2Decision": "Log warning and select from global provider pool",
+      "scenario3Title": "Health Filtering (Circuit Breaker + Rate Limit)",
+      "scenario3Desc": "System auto filters circuit-broken or over-limit providers",
+      "scenario3Step1": "Circuit Breaker Check",
+      "scenario3Step1Desc": "Circuit breaker opens after 5 consecutive failures, unavailable for 60s",
+      "scenario3Step1Before": "Provider A failed 5 times, circuit breaker: open",
+      "scenario3Step1After": "A filtered, remaining: B, C, D",
+      "scenario3Step1Decision": "A auto recovers to half-open after 60s",
+      "scenario3Step2": "Amount Rate Limit",
+      "scenario3Step2Desc": "Check if spending exceeds limits (5h, 7d, 30d)",
+      "scenario3Step2Before": "Provider B 5h limit $10, consumed $9.8",
+      "scenario3Step2After": "B filtered (near limit), remaining: C, D",
+      "scenario3Step2Decision": "Auto recovery after 5h sliding window",
+      "scenario3Step3": "Concurrent Session Limit",
+      "scenario3Step3Desc": "Check if active session count exceeds configured concurrent limit",
+      "scenario3Step3Before": "Provider C concurrent limit 2, currently 2 active sessions",
+      "scenario3Step3After": "C filtered (full), remaining: D",
+      "scenario3Step3Decision": "Auto release after session expiry (5 min)",
+      "scenario4Title": "Session Reuse Mechanism",
+      "scenario4Desc": "Consecutive chats prioritize using same provider, leveraging Claude context cache",
+      "scenario4Step1": "Check Request History",
+      "scenario4Step1Desc": "Query providers used by this API Key in last 10 seconds",
+      "scenario4Step1Before": "Last request used provider B",
+      "scenario4Step1After": "Check if B is enabled and healthy",
+      "scenario4Step1Decision": "B available, reuse directly, skip random selection",
+      "scenario4Step2": "Reuse Invalidation",
+      "scenario4Step2Desc": "If last used provider unavailable, reselect",
+      "scenario4Step2Before": "Last used provider B disabled or circuit-broken",
+      "scenario4Step2After": "Enter normal selection flow",
+      "scenario4Step2Decision": "Select from other available providers",
+      "step": "Step",
+      "before": "Before:",
+      "after": "After:",
+      "decision": "Decision:"
+    },
+    "section": {
+      "title": "Provider Management",
+      "description": "Configure upstream provider rate limiting and concurrent session limits. Leave empty for unlimited."
+    },
+    "addProvider": "Add Provider",
+    "editProvider": "Edit Provider",
+    "createProvider": "Add Provider",
+    "updateFailed": "Failed to update provider",
+    "deleteSuccess": "Deleted successfully",
+    "confirmDeleteDesc": "Are you sure you want to delete provider \"{name}\"? This action cannot be undone.",
+    "confirmDeleteProvider": "Confirm Delete Provider?",
+    "confirmDeleteProviderDesc": "Are you sure you want to delete provider \"{name}\"? This action is irreversible.",
+    "noProviders": "No providers configured",
+    "noProvidersDesc": "Add your first API provider",
+    "toggleSuccess": "Provider {status}",
+    "toggleSuccessDesc": "Provider \"{name}\" status updated",
+    "toggleFailed": "Toggle failed",
+    "enabledStatus": "enabled",
+    "disabledStatus": "disabled",
+    "searchResults": "Found {count} matching providers",
+    "searchNoResults": "No matching providers found",
+    "displayCount": "Showing {filtered} / {total} providers",
+    "todayUsage": "Today's Usage",
+    "todayUsageCount": "{count} times",
+    "circuitBroken": "Circuit Broken",
+    "official": "Official",
+    "viewKey": "View Complete API Key",
+    "viewKeyDesc": "Please keep it safe and don't share it with others",
+    "keyLoading": "Loading...",
+    "resetCircuit": "Circuit breaker reset",
+    "resetCircuitDesc": "Provider \"{name}\" circuit breaker status cleared",
+    "resetCircuitFailed": "Failed to reset circuit breaker"
   },
   "prices": {
     "title": "Pricing",
     "description": "Manage platform basic configuration and model pricing",
     "subtitle": "Model Pricing",
     "subtitleDesc": "Manage AI model pricing configuration",
-    "search": "Search model name...",
-    "sync": "Sync LiteLLM Prices",
+    "search": {
+      "placeholder": "Search model name...",
+      "totalModels": "{total} total model prices",
+      "searchResults": "(Search results: {count})",
+      "lastUpdated": "Last updated:"
+    },
+    "sync": {
+      "button": "Sync LiteLLM Prices",
+      "syncing": "Syncing...",
+      "successWithChanges": "Sync successful: Added {added}, Updated {updated}, Unchanged {unchanged}",
+      "successNoChanges": "All {unchanged} model prices are up to date",
+      "noModels": "No supported model prices found",
+      "failed": "Sync failed",
+      "failedError": "Sync failed, please retry",
+      "failedNoResult": "Sync successful but no result returned",
+      "partialFailure": "{failed} models failed to process"
+    },
     "syncing": "Syncing...",
     "syncSuccess": "Price table updated successfully",
     "syncFailed": "Sync failed",
     "syncFailedError": "Sync failed:",
     "syncNoResult": "Price table updated but no result returned",
-    "upload": "Update Model Price Table",
+    "upload": {
+      "button": "Update Price Table",
+      "updating": "Updating...",
+      "dialogTitle": "Update Model Price Table",
+      "dialogDescription": "Select a JSON file containing model price data to update the price table",
+      "selectFile": "Click to select JSON file or drag and drop here",
+      "fileSizeLimit": "File size must not exceed 10MB",
+      "selectFileButton": "Select File",
+      "invalidFileType": "Please select a JSON file",
+      "fileTooLarge": "File size must not exceed 10MB",
+      "success": "Price table updated successfully",
+      "failed": "Update failed, please retry",
+      "failedError": "Update failed:",
+      "noResult": "Price table updated successfully but no result returned",
+      "updateInProgress": "Updating model prices...",
+      "enterDashboard": "Enter Dashboard",
+      "completed": "Completed"
+    },
     "uploadSuccess": "Price table updated successfully",
     "uploadFailed": "Failed to get pricing data:",
     "noData": "System has built-in price table. Use buttons above to sync or update.",
     "noModels": "No model prices found",
     "table": {
-      "model": "Model",
-      "inputPrice": "Input Price",
-      "outputPrice": "Output Price",
-      "cachePrice": "Cache Price",
-      "updatedAt": "Updated At"
+      "modelName": "Model Name",
+      "type": "Type",
+      "provider": "Provider",
+      "inputPrice": "Input Price ($/M)",
+      "outputPrice": "Output Price ($/M)",
+      "updatedAt": "Updated At",
+      "typeChat": "Chat",
+      "typeImageGen": "Image Generation",
+      "typeCompletion": "Completion",
+      "typeUnknown": "Unknown",
+      "imagePrice": "${price}/img",
+      "loading": "Loading...",
+      "noResults": "No matching models found",
+      "noData": "No price data available",
+      "noDataHint": "Built-in price table available. Use buttons above to sync or update"
     },
     "dialog": {
       "title": "Update Model Price Table",
@@ -230,6 +516,33 @@
       "uploading": "Uploading...",
       "readError": "Failed to get key",
       "getError": "Failed to get key"
+    },
+    "section": {
+      "title": "Model Pricing",
+      "description": "Manage AI model pricing configuration"
+    },
+    "pagination": {
+      "perPage": "Per page:",
+      "showing": "Showing {start} - {end} of {total} records",
+      "previous": "Previous",
+      "next": "Next"
+    },
+    "uploadDialog": {
+      "notes": {
+        "litellm": "• Built-in LiteLLM price table. Use 'Sync LiteLLM Prices' button on the left to update",
+        "manual": "• You can also manually download",
+        "link": "latest price table",
+        "linkAnd": "and update via this button",
+        "support": "• Supports Claude and OpenAI models (claude-, gpt-, o1-, o3- prefix)"
+      },
+      "result": {
+        "total": "Total Processed",
+        "added": "Models Added ({count})",
+        "updated": "Models Updated ({count})",
+        "unchanged": "Unchanged ({count})",
+        "failed": "Failed ({count})",
+        "showMore": "and {count} more"
+      }
     }
   },
   "sensitiveWords": {
@@ -276,6 +589,43 @@
       "totalChecks": "Total Checks",
       "cacheSize": "Cache Size",
       "lastUpdated": "Last Updated"
+    },
+    "section": {
+      "title": "Sensitive Words List",
+      "description": "Requests blocked by sensitive words will not be forwarded upstream and will not be charged. Supports contains matching, exact matching, and regex patterns."
+    },
+    "refreshCacheSuccess": "Cache refreshed successfully, loaded {count} sensitive words",
+    "refreshCacheFailed": "Failed to refresh cache",
+    "cacheStats": "Cache stats: Contains({containsCount}) Exact({exactCount}) Regex({regexCount})",
+    "emptyState": "No sensitive words yet. Click 'Add Sensitive Word' in the top right to start configuration.",
+    "confirmDelete": "Are you sure you want to delete the sensitive word \"{word}\"?",
+    "dialog": {
+      "addTitle": "Add Sensitive Word",
+      "addDescription": "Configure sensitive word filtering rules. Matched requests will not be forwarded upstream.",
+      "editTitle": "Edit Sensitive Word",
+      "editDescription": "Modify sensitive word configuration. Changes will automatically refresh the cache.",
+      "wordLabel": "Sensitive Word *",
+      "wordPlaceholder": "Enter sensitive word...",
+      "wordRequired": "Please enter a sensitive word",
+      "matchTypeLabel": "Match Type *",
+      "matchTypeContains": "Contains Match - Block if text contains this word",
+      "matchTypeExact": "Exact Match - Block only if exact match",
+      "matchTypeRegex": "Regular Expression - Support complex pattern matching",
+      "descriptionLabel": "Description",
+      "descriptionPlaceholder": "Optional: Add description...",
+      "creating": "Creating...",
+      "saving": "Saving..."
+    },
+    "table": {
+      "word": "Sensitive Word",
+      "matchType": "Match Type",
+      "matchTypeContains": "Contains Match",
+      "matchTypeExact": "Exact Match",
+      "matchTypeRegex": "Regular Expression",
+      "description": "Description",
+      "status": "Status",
+      "createdAt": "Created At",
+      "actions": "Actions"
     }
   },
   "logs": {
@@ -343,21 +693,52 @@
   "clientVersions": {
     "title": "Client Update Reminder",
     "description": "Manage client version requirements to ensure users use latest stable version. VSCode plugin and CLI are managed separately.",
+    "section": {
+      "settings": {
+        "title": "Update Reminder Settings",
+        "description": "When enabled, system automatically detects client version and blocks old version users."
+      },
+      "distribution": {
+        "title": "Client Version Distribution",
+        "description": "Shows client version info for active users in past 7 days. Each client type independently tracks GA versions."
+      }
+    },
+    "empty": {
+      "title": "No client data available",
+      "description": "No active users using recognizable clients in past 7 days"
+    },
     "toggle": {
-      "title": "Update Reminder Settings",
-      "description": "When enabled, system automatically detects client version and blocks old version users.",
-      "note": "Note: VSCode plugin (claude-vscode) and CLI (claude-cli) use independent version detection strategies.",
-      "enable": "Enable Client Version Check",
-      "disable": "Disable Client Version Check",
+      "enable": "Enable Update Reminder",
+      "description": "When enabled, system will block requests from old version clients",
       "enableSuccess": "Client version check enabled",
       "disableSuccess": "Client version check disabled",
-      "toggleFailed": "Toggle failed"
+      "toggleFailed": "Update failed"
+    },
+    "features": {
+      "title": "Feature Description",
+      "whatHappens": "What happens when enabled:",
+      "autoDetect": "System automatically detects the latest stable version (GA version) for each client type",
+      "gaRule": "GA Rule: ",
+      "gaRuleDesc": "A version is considered GA when used by more than 1 user",
+      "activeWindow": "Active Window: ",
+      "activeWindowDesc": "Only counts users with requests in the past 7 days",
+      "blockOldVersion": "Users with old versions will receive HTTP 400 error and cannot continue using the service",
+      "errorMessage": "Error message includes current version and required upgrade version",
+      "recommendation": "Recommendation: ",
+      "recommendationDesc": "Monitor the version distribution below and confirm new version stability before enabling."
     },
-    "stats": {
-      "title": "Client Version Distribution",
-      "description": "Shows client version info for active users in past 7 days. Each client type independently tracks GA versions.",
-      "noData": "No client data available",
-      "noDataDesc": "No active users using recognizable clients in past 7 days"
+    "table": {
+      "internalType": "Internal Type: ",
+      "currentGA": "Current GA Version: ",
+      "usersCount": "{count} users",
+      "user": "User",
+      "version": "Current Version",
+      "lastActive": "Last Active",
+      "status": "Status",
+      "noUsers": "No user data available",
+      "latest": "Latest",
+      "needsUpgrade": "Needs Upgrade",
+      "unknown": "Unknown"
     }
   },
   "notifications": {
@@ -433,4 +814,4 @@
     "loadFailed": "Failed to load notification settings",
     "unknownError": "An error occurred during operation"
   }
-}
+}

+ 19 - 1
messages/en/usage.json

@@ -391,6 +391,23 @@
           "Start using!"
         ]
       }
+    },
+
+    "startup": {
+      "title": "Start droid",
+      "description": "Run the following command in your project directory:",
+      "initNote": "During the first startup, droid will perform initial configuration."
+    },
+
+    "commonIssues": {
+      "title": "Common questions",
+      "commandNotFound": "1. Command not found",
+      "commandNotFoundWindows": [
+        "Make sure the npm global path (usually C:\\Users\\your-username\\AppData\\Roaming\\npm) has been added to the system PATH",
+        "Reopen the PowerShell window"
+      ],
+      "commandNotFoundUnix": "Check the npm global installation path and add it to PATH (if not already there)",
+      "updateCli": "2. Update droid"
     }
   },
 
@@ -448,6 +465,7 @@
 
   "ui": {
     "mainContent": "Documentation content",
-    "main": "main"
+    "main": "main",
+    "currentSiteAddress": "Current site address"
   }
 }

+ 2 - 0
messages/ja/index.ts

@@ -10,6 +10,7 @@ import settings from "./settings.json";
 import ui from "./ui.json";
 import usage from "./usage.json";
 import validation from "./validation.json";
+import internal from "./internal.json";
 
 export default {
   auth,
@@ -24,4 +25,5 @@ export default {
   ui,
   usage,
   validation,
+  internal,
 };

+ 83 - 0
messages/ja/internal.json

@@ -0,0 +1,83 @@
+{
+  "dataGenerator": {
+    "tabs": {
+      "usage": "使用量データ生成",
+      "userBreakdown": "ユーザー別使用量表示"
+    },
+    "actions": {
+      "reconfigure": "パラメータを再設定",
+      "generate": "データ生成",
+      "exportScreenshot": "スクリーンショットをエクスポート",
+      "exportPDF": "PDFをエクスポート"
+    },
+    "params": {
+      "title": "生成パラメータ",
+      "description": "シミュレートログデータを作成するためのパラメータを設定",
+      "startDate": "開始時刻",
+      "endDate": "終了時刻",
+      "totalCostCny": "合計金額(人民元)",
+      "totalRecords": "総レコード数",
+      "models": "含まれるモデル(カンマ区切り)",
+      "userIds": "ユーザーID(カンマ区切り)",
+      "providerIds": "プロバイダーID(カンマ区切り)",
+      "serviceName": "サービス名",
+      "required": "*",
+      "placeholders": {
+        "totalCostCny": "例:1000",
+        "totalRecords": "例:500(空欄の場合、金額から計算)",
+        "models": "例:claude-3-5-sonnet,gpt-4(空欄の場合、全て)",
+        "userIds": "例:1,2,3(空欄の場合、全て)",
+        "providerIds": "例:1,2(空欄の場合、全て)",
+        "serviceName": "AI大規模モデル推論サービス"
+      }
+    },
+    "summary": {
+      "totalRecords": "総レコード数",
+      "totalCost": "総コスト",
+      "totalCostCny": "総コスト(人民元)",
+      "totalTokens": "総トークン",
+      "timeRange": "時間範囲",
+      "to": "から",
+      "uniqueUsers": "総ユーザー数",
+      "totalCalls": "総呼び出し数"
+    },
+    "table": {
+      "usageLogs": {
+        "title": "使用ログ",
+        "description": "合計 {count} 件のレコード",
+        "columns": {
+          "time": "時刻",
+          "user": "ユーザー",
+          "key": "キー",
+          "provider": "プロバイダー",
+          "model": "モデル",
+          "input": "入力",
+          "output": "出力",
+          "cacheWrite": "キャッシュ書き込み",
+          "cacheRead": "キャッシュ読み取り",
+          "cost": "コスト",
+          "duration": "所要時間",
+          "status": "ステータス"
+        }
+      },
+      "userBreakdown": {
+        "title": "ユーザー使用量詳細",
+        "description": "合計 {count} 件のレコード",
+        "collapseByUser": "ユーザーごとに折りたたむ",
+        "columns": {
+          "userName": "ユーザー名",
+          "key": "キー",
+          "serviceModel": "サービスモデル",
+          "totalCalls": "総呼び出し数",
+          "totalCost": "総コスト"
+        }
+      }
+    },
+    "status": {
+      "success": "成功"
+    },
+    "errors": {
+      "failed": "ログの生成に失敗しました"
+    }
+  }
+}

+ 177 - 27
messages/ja/quota.json

@@ -1,55 +1,205 @@
 {
   "header": {
-    "title": "ユーザークォータ",
+    "title": "用户配额",
     "role": {
-      "admin": "管理",
-      "user": "ユーザー"
+      "admin": "管理",
+      "user": "用户"
     },
-    "keysCountSuffix": "個のキー",
-    "rpm": "リクエスト制限",
-    "todayCost": "本日の消費量",
-    "exceededNotice": "クォータを超過しています。管理者にお問い合わせください"
+    "keysCountSuffix": "个密钥",
+    "rpm": "请求限流",
+    "todayCost": "今日消费",
+    "exceededNotice": "已超过限额,请联系管理员处理"
   },
   "countdown": {
-    "reset": "リセットカウントダウン"
+    "reset": "重置倒计时"
   },
   "windowType": {
     "5h": {
-      "label": "5時間ローリング",
-      "description": "過去5時間の消費を追跡し、1時間ごとに更新されます"
+      "label": "5小时滚动",
+      "description": "统计过去5小时内的消费,每小时滚动更新"
     },
     "weekly": {
-      "label": "週単位リセット",
-      "description": "カレンダー週(月曜日00:00リセット)、1週間全体を集計"
+      "label": "每周重置",
+      "description": "自然周制(周一00:00重置),整周统计"
     },
     "monthly": {
-      "label": "月単位リセット",
-      "description": "カレンダー月(毎月1日00:00リセット)、1ヶ月全体を集計"
+      "label": "每月重置",
+      "description": "自然月制(每月1日00:00重置),整月统计"
     },
     "daily": {
-      "label": "日単位リセット",
-      "description": "カレンダー日(毎日00:00リセット)、日ごとに集計"
+      "label": "每日重置",
+      "description": "自然日制(每日00:00重置),按日统计"
     }
   },
   "toolbar": {
-    "searchPlaceholder": "ユーザー名またはキーで検索",
-    "filter": "フィルター",
-    "sort": "並べ替え",
-    "refresh": "新",
-    "autoRefresh": "自動更新",
+    "searchPlaceholder": "搜索用户名或密钥",
+    "filter": "筛选",
+    "sort": "排序",
+    "refresh": "新",
+    "autoRefresh": "自动刷新",
     "filterOptions": {
-      "all": "すべて",
-      "warning": "警",
-      "exceeded": "超過"
+      "all": "全部",
+      "warning": "警",
+      "exceeded": "已超限"
     },
     "sortOptions": {
-      "name": "名前順",
-      "usage": "使用率"
+      "name": "按名称",
+      "usage": "使用率"
     },
     "interval": {
       "10s": "10秒",
       "30s": "30秒",
       "60s": "60秒"
     }
+  },
+  "layout": {
+    "title": "クォータ管理",
+    "description": "すべてのレベルでクォータ使用状況を表示および管理",
+    "tabs": {
+      "users": "ユーザークォータ",
+      "keys": "キークォータ",
+      "providers": "プロバイダークォータ"
+    }
+  },
+  "users": {
+    "title": "ユーザークォータ統計",
+    "totalCount": "合計 {count} 人のユーザー",
+    "noNote": "備考なし",
+    "rpm": {
+      "label": "RPM クォータ",
+      "description": "1分あたりのリクエスト数"
+    },
+    "dailyCost": {
+      "label": "日次コスト",
+      "resetAt": "リセット時刻"
+    },
+    "noQuotaData": "クォータ情報を取得できません",
+    "noMatches": "一致するユーザーが見つかりません",
+    "noData": "ユーザーデータがありません",
+    "sort": {
+      "name": "名前順",
+      "usage": "使用率順"
+    },
+    "filter": {
+      "all": "すべて",
+      "warning": "制限に近い (>60%)",
+      "exceeded": "超過 (≥100%)"
+    }
+  },
+  "providers": {
+    "title": "プロバイダークォータ統計",
+    "totalCount": "合計 {count} 個のプロバイダー",
+    "filterCount": "{filtered} / {total} 個のプロバイダーを表示",
+    "status": {
+      "enabled": "有効",
+      "disabled": "無効"
+    },
+    "card": {
+      "priority": "優先度",
+      "weight": "重み"
+    },
+    "cost5h": {
+      "label": "5時間コスト"
+    },
+    "costWeekly": {
+      "label": "週次コスト",
+      "resetAt": "リセット時刻"
+    },
+    "costMonthly": {
+      "label": "月次コスト",
+      "resetAt": "リセット時刻"
+    },
+    "concurrentSessions": {
+      "label": "同時セッション"
+    },
+    "noQuotaSet": "クォータ未設定",
+    "noQuotaData": "クォータ情報を取得できません",
+    "noMatches": "一致するプロバイダーがありません",
+    "unlimitedSection": "クォータ未設定のプロバイダー ({count}個)"
+  },
+  "keys": {
+    "title": "キークォータ統計",
+    "totalCount": "合計 {users} 人のユーザー、{keys} 個のキー",
+    "searchPlaceholder": "ユーザーまたはキーを検索...",
+    "filterLabel": "フィルター条件",
+    "filterCount": "{users} 人のユーザー、{keys} 個のキーを表示",
+    "filter": {
+      "all": "すべてのキー",
+      "keyQuota": "キークォータのみ",
+      "userQuotaOnly": "ユーザークォータのみ",
+      "warning": "警告 (≥60%)",
+      "exceeded": "超過 (≥100%)"
+    },
+    "table": {
+      "keyName": "キー名",
+      "quotaType": "クォータタイプ",
+      "cost5h": "5時間クォータ",
+      "costWeekly": "週次クォータ",
+      "costMonthly": "月次クォータ",
+      "concurrentSessions": "同時制限",
+      "status": "ステータス",
+      "actions": "アクション"
+    },
+    "quotaType": {
+      "independent": "独立クォータ",
+      "inherited": "ユーザーから継承"
+    },
+    "status": {
+      "disabled": "無効",
+      "restricted": "制限中",
+      "normal": "正常"
+    },
+    "noMatches": "一致するユーザーまたはキーがありません",
+    "editDialog": {
+      "title": "キークォータ設定",
+      "description": "密钥: {keyName} ({userName})",
+      "cost5h": {
+        "label": "5小时限额(USD)",
+        "placeholder": "不限制",
+        "current": "当前已用: {currency}{current} / {currency}{limit}"
+      },
+      "costWeekly": {
+        "label": "周限额(USD)",
+        "placeholder": "不限制",
+        "current": "当前已用: {currency}{current} / {currency}{limit}"
+      },
+      "costMonthly": {
+        "label": "月限额(USD)",
+        "placeholder": "不限制",
+        "current": "当前已用: {currency}{current} / {currency}{limit}"
+      },
+      "concurrentSessions": {
+        "label": "并发 Session 限额",
+        "placeholder": "0 = 不限制",
+        "current": "当前并发: {current} / {limit}"
+      },
+      "clearAll": "清除所有限额",
+      "save": "保存",
+      "setQuota": "设置限额",
+      "success": "クォータが正常に設定されました",
+      "clearSuccess": "限额已清除",
+      "error": "设置失败",
+      "clearError": "清除失败",
+      "retryError": "设置失败,请稍后重试"
+    },
+    "editUserDialog": {
+      "title": "ユーザークォータ設定",
+      "description": "用户: {userName}",
+      "rpm": {
+        "label": "每分钟请求数 (RPM)",
+        "placeholder": "60",
+        "current": "当前: {current} / {limit} 请求/分钟"
+      },
+      "dailyQuota": {
+        "label": "每日消费限额(USD)",
+        "placeholder": "100",
+        "current": "今日已用: {currency}{current} / {currency}{limit}"
+      },
+      "save": "保存",
+      "editQuota": "编辑限额",
+      "success": "用户限额设置成功",
+      "error": "设置失败",
+      "retryError": "设置失败,请稍后重试"
+    }
   }
-}
+}

+ 115 - 9
messages/ja/settings.json

@@ -201,24 +201,66 @@
     "description": "プラットフォーム基本設定とモデル価格を管理します",
     "subtitle": "モデル価格",
     "subtitleDesc": "AIモデルの価格設定を管理します",
-    "search": "モデル名で検索...",
-    "sync": "LiteLLM価格を同期",
+    "search": {
+      "placeholder": "モデル名を検索...",
+      "totalModels": "合計{total}件のモデル価格",
+      "searchResults": "(検索結果:{count}件)",
+      "lastUpdated": "最終更新:"
+    },
+    "sync": {
+      "button": "LiteLLM価格を同期",
+      "syncing": "同期中...",
+      "successWithChanges": "同期成功:追加{added}件、更新{updated}件、未変更{unchanged}件",
+      "successNoChanges": "すべて{unchanged}件のモデル価格は最新です",
+      "noModels": "サポートされているモデル価格が見つかりません",
+      "failed": "同期に失敗しました",
+      "failedError": "同期に失敗しました。再試行してください",
+      "failedNoResult": "同期は成功しましたが、結果が返されませんでした",
+      "partialFailure": "{failed}件のモデル処理に失敗しました"
+    },
     "syncing": "同期中...",
     "syncSuccess": "価格表が正常に更新されました",
     "syncFailed": "同期に失敗しました",
     "syncFailedError": "同期に失敗しました:",
     "syncNoResult": "価格表は更新されましたが結果が返されていません",
-    "upload": "モデル価格表を更新",
+    "upload": {
+      "button": "価格表を更新",
+      "updating": "更新中...",
+      "dialogTitle": "モデル価格表を更新",
+      "dialogDescription": "モデル価格データを含むJSONファイルを選択して価格表を更新",
+      "selectFile": "クリックしてJSONファイルを選択するか、ここにドラッグ&ドロップ",
+      "fileSizeLimit": "ファイルサイズは10MBを超えないでください",
+      "selectFileButton": "ファイルを選択",
+      "invalidFileType": "JSONファイルを選択してください",
+      "fileTooLarge": "ファイルサイズは10MBを超えないでください",
+      "success": "価格表の更新に成功しました",
+      "failed": "更新に失敗しました。再試行してください",
+      "failedError": "更新に失敗しました:",
+      "noResult": "価格表の更新に成功しましたが、結果が返されませんでした",
+      "updateInProgress": "モデル価格を更新中...",
+      "enterDashboard": "ダッシュボードに移動",
+      "completed": "完了"
+    },
     "uploadSuccess": "価格表が正常に更新されました",
     "uploadFailed": "価格データの取得に失敗しました:",
     "noData": "システムは組み込み価格表を持っています。上のボタンを使用して同期または更新してください。",
     "noModels": "モデル価格が見つかりません",
     "table": {
-      "model": "モデル",
-      "inputPrice": "入力価格",
-      "outputPrice": "出力価格",
-      "cachePrice": "キャッシュ価格",
-      "updatedAt": "更新日"
+      "modelName": "モデル名",
+      "type": "タイプ",
+      "provider": "プロバイダー",
+      "inputPrice": "入力価格 ($/M)",
+      "outputPrice": "出力価格 ($/M)",
+      "updatedAt": "更新日時",
+      "typeChat": "チャット",
+      "typeImageGen": "画像生成",
+      "typeCompletion": "補完",
+      "typeUnknown": "不明",
+      "imagePrice": "${price}/img",
+      "loading": "読み込み中...",
+      "noResults": "一致するモデルが見つかりません",
+      "noData": "価格データがありません",
+      "noDataHint": "組み込みの価格表が利用可能です。上記のボタンを使用して同期または更新してください"
     },
     "dialog": {
       "title": "モデル価格表を更新",
@@ -230,6 +272,33 @@
       "uploading": "アップロード中...",
       "readError": "キーの取得に失敗しました",
       "getError": "キーの取得に失敗しました"
+    },
+    "section": {
+      "title": "モデル価格",
+      "description": "AIモデルの価格設定を管理"
+    },
+    "pagination": {
+      "perPage": "ページあたり:",
+      "showing": "{total}件中{start} - {end}件を表示",
+      "previous": "前へ",
+      "next": "次へ"
+    },
+    "uploadDialog": {
+      "notes": {
+        "litellm": "• 組み込みのLiteLLM価格表があります。左側の「LiteLLM価格を同期」ボタンを使用して更新できます",
+        "manual": "• 手動でダウンロードすることもできます",
+        "link": "最新の価格表",
+        "linkAnd": "このボタンから更新",
+        "support": "• ClaudeとOpenAIモデルをサポート(claude-, gpt-, o1-, o3-プレフィックス)"
+      },
+      "result": {
+        "total": "処理総数",
+        "added": "追加されたモデル({count}件)",
+        "updated": "更新されたモデル({count}件)",
+        "unchanged": "未変更({count}件)",
+        "failed": "失敗({count}件)",
+        "showMore": "他{count}件"
+      }
     }
   },
   "sensitiveWords": {
@@ -276,6 +345,43 @@
       "totalChecks": "総チェック数",
       "cacheSize": "キャッシュサイズ",
       "lastUpdated": "最終更新"
+    },
+    "section": {
+      "title": "センシティブワードリスト",
+      "description": "センシティブワードによってブロックされたリクエストはアップストリームに転送されず、課金もされません。部分一致、完全一致、正規表現の3つのモードをサポートしています。"
+    },
+    "refreshCacheSuccess": "キャッシュのリフレッシュに成功しました。{count}個のセンシティブワードを読み込みました",
+    "refreshCacheFailed": "キャッシュのリフレッシュに失敗しました",
+    "cacheStats": "キャッシュ統計:部分一致({containsCount}) 完全一致({exactCount}) 正規表現({regexCount})",
+    "emptyState": "センシティブワードがありません。右上の「センシティブワードを追加」をクリックして設定を開始してください。",
+    "confirmDelete": "センシティブワード「{word}」を削除してもよろしいですか?",
+    "dialog": {
+      "addTitle": "センシティブワードを追加",
+      "addDescription": "センシティブワードフィルタリングルールを設定します。マッチしたリクエストはアップストリームに転送されません。",
+      "editTitle": "センシティブワードを編集",
+      "editDescription": "センシティブワード設定を変更します。変更後、自動的にキャッシュがリフレッシュされます。",
+      "wordLabel": "センシティブワード *",
+      "wordPlaceholder": "センシティブワードを入力...",
+      "wordRequired": "センシティブワードを入力してください",
+      "matchTypeLabel": "マッチタイプ *",
+      "matchTypeContains": "部分一致 - テキストにこの単語が含まれている場合ブロック",
+      "matchTypeExact": "完全一致 - 完全に一致する場合のみブロック",
+      "matchTypeRegex": "正規表現 - 複雑なパターンマッチングをサポート",
+      "descriptionLabel": "説明",
+      "descriptionPlaceholder": "オプション:説明を追加...",
+      "creating": "作成中...",
+      "saving": "保存中..."
+    },
+    "table": {
+      "word": "センシティブワード",
+      "matchType": "マッチタイプ",
+      "matchTypeContains": "部分一致",
+      "matchTypeExact": "完全一致",
+      "matchTypeRegex": "正規表現",
+      "description": "説明",
+      "status": "ステータス",
+      "createdAt": "作成日時",
+      "actions": "操作"
     }
   },
   "logs": {
@@ -433,4 +539,4 @@
     "loadFailed": "通知設定の読み込みに失敗しました",
     "unknownError": "操作中にエラーが発生しました"
   }
-}
+}

+ 19 - 1
messages/ja/usage.json

@@ -391,6 +391,23 @@
           "使用開始!"
         ]
       }
+    },
+
+    "startup": {
+      "title": "droid を起動",
+      "description": "プロジェクトディレクトリで以下のコマンドを実行:",
+      "initNote": "初回起動時、droid は初期設定を実行します。"
+    },
+
+    "commonIssues": {
+      "title": "よくある質問",
+      "commandNotFound": "1. コマンドが見つかりません",
+      "commandNotFoundWindows": [
+        "npm グローバルパス (通常 C:\\Users\\your-username\\AppData\\Roaming\\npm) がシステム PATH に追加されていることを確認",
+        "PowerShell ウィンドウを再起動"
+      ],
+      "commandNotFoundUnix": "npm グローバルインストールパスを確認し、PATH に追加 (まだの場合)",
+      "updateCli": "2. droid を更新"
     }
   },
 
@@ -448,6 +465,7 @@
 
   "ui": {
     "mainContent": "ドキュメントコンテンツ",
-    "main": "main"
+    "main": "main",
+    "currentSiteAddress": "現在のサイトアドレス"
   }
 }

+ 516 - 0
messages/providers-i18n-additions.json

@@ -0,0 +1,516 @@
+{
+  "zh-CN": {
+    "providers": {
+      "section": {
+        "title": "服务商管理",
+        "description": "配置上游服务商的金额限流和并发限制,留空表示无限制。"
+      },
+      "addProvider": "新增服务商",
+      "editProvider": "编辑服务商",
+      "createProvider": "新增服务商",
+      "updateFailed": "更新服务商失败",
+      "deleteSuccess": "删除成功",
+      "confirmDeleteDesc": "确定要删除供应商 \"{name}\" 吗?此操作无法撤销。",
+      "confirmDeleteProvider": "确认删除供应商?",
+      "confirmDeleteProviderDesc": "确定要删除服务商\"{name}\"吗?此操作不可恢复。",
+      "noProviders": "暂无服务商配置",
+      "noProvidersDesc": "添加你的第一个 API 服务商",
+      "toggleSuccess": "供应商已{status}",
+      "toggleSuccessDesc": "供应商 \"{name}\" 状态已更新",
+      "toggleFailed": "状态切换失败",
+      "enabledStatus": "启用",
+      "disabledStatus": "禁用",
+      "searchResults": "找到 {count} 个匹配的供应商",
+      "searchNoResults": "未找到匹配的供应商",
+      "displayCount": "显示 {filtered} / {total} 个供应商",
+      "todayUsage": "今日用量",
+      "todayUsageCount": "{count} 次",
+      "circuitBroken": "熔断中",
+      "official": "官网",
+      "viewKey": "查看完整 API Key",
+      "viewKeyDesc": "请妥善保管,不要泄露给他人",
+      "keyLoading": "加载中...",
+      "resetCircuit": "熔断器已重置",
+      "resetCircuitDesc": "供应商 \"{name}\" 的熔断状态已解除",
+      "resetCircuitFailed": "重置熔断器失败",
+      "form": {
+        "providerName": "服务商名称",
+        "providerNameRequired": "服务商名称 *",
+        "providerNamePlaceholder": "例如: 智谱",
+        "apiAddress": "API 地址",
+        "apiAddressRequired": "API 地址 *",
+        "apiAddressPlaceholder": "例如: https://open.bigmodel.cn/api/anthropic",
+        "apiKeyRequired": "API 密钥 *",
+        "apiKeyLeaveEmpty": "(留空不更改)",
+        "apiKeyLeaveEmptyDesc": "留空则不更改密钥",
+        "apiKeyCurrent": "当前密钥:",
+        "websiteUrl": "供应商官网地址",
+        "websiteUrlPlaceholder": "https://example.com",
+        "websiteUrlDesc": "供应商官网地址,用于快速跳转管理",
+        "websiteUrlInvalid": "请输入有效的供应商官网地址",
+        "expandAll": "展开全部高级配置",
+        "collapseAll": "折叠全部高级配置",
+        "routingConfig": "路由配置",
+        "routingConfigSummary": "{models} 个模型白名单, {redirects} 个重定向",
+        "routingConfigNone": "未配置",
+        "providerTypeDesc": "选择供应商的 API 格式类型。",
+        "providerTypeDisabledNote": "注:Gemini CLI 和 OpenAI Compatible 类型功能正在开发中,暂不可用",
+        "modelRedirectsLabel": "模型重定向配置",
+        "modelRedirectsOptional": "(可选)",
+        "modelRedirectsDesc": "将 Claude Code 客户端请求的模型(如 claude-sonnet-4.5)重定向到上游供应商实际支持的模型(如 glm-4.6、gemini-pro)。用于成本优化或接入第三方 AI 服务。",
+        "modelRedirectsEmpty": "暂无重定向规则。添加规则后,系统将自动重写请求中的模型名称。",
+        "modelRedirectsCurrentRules": "当前规则 ({count})",
+        "modelRedirectsAddNew": "添加新规则",
+        "modelRedirectsSourceModel": "用户请求的模型",
+        "modelRedirectsSourcePlaceholder": "例如: claude-sonnet-4-5-20250929",
+        "modelRedirectsTargetModel": "实际转发的模型",
+        "modelRedirectsTargetPlaceholder": "例如: glm-4.6",
+        "modelRedirectsSourceRequired": "源模型名称不能为空",
+        "modelRedirectsTargetRequired": "目标模型名称不能为空",
+        "modelRedirectsExists": "模型 \"{model}\" 已存在重定向规则",
+        "joinClaudePool": "加入 Claude 调度池",
+        "joinClaudePoolDesc": "启用后,此供应商将与 Claude 类型供应商一起参与负载均衡调度",
+        "joinClaudePoolHelp": "仅当模型重定向配置中存在映射到 claude-* 模型时可用。启用后,当用户请求 claude-* 模型时,此供应商也会参与调度选择。",
+        "modelWhitelist": "模型白名单",
+        "modelWhitelistDesc": "限制此供应商可以处理的模型。默认情况下,供应商可以处理该类型下的所有模型。",
+        "modelWhitelistLabel": "允许的模型",
+        "modelWhitelistSelected": "已选择 {count} 个模型",
+        "modelWhitelistLoading": "加载中...",
+        "modelWhitelistNotFound": "未找到模型",
+        "modelWhitelistSearchPlaceholder": "搜索模型名称...",
+        "modelWhitelistSelectAll": "全选 ({count})",
+        "modelWhitelistClear": "清空",
+        "modelWhitelistManualAdd": "手动添加模型",
+        "modelWhitelistManualPlaceholder": "输入模型名称(如 gpt-5-turbo)",
+        "modelWhitelistManualDesc": "支持添加任意模型名称(不限于价格表中的模型)",
+        "modelWhitelistAllowAll": "允许所有 {type} 模型",
+        "modelWhitelistAllowAllClause": "允许所有 Claude 模型",
+        "modelWhitelistAllowAllOpenAI": "允许所有 OpenAI 模型",
+        "modelWhitelistSelectedOnly": "仅允许选中的 {count} 个模型。其他模型的请求不会调度到此供应商。",
+        "scheduleParams": "调度参数",
+        "priorityLabel": "优先级",
+        "priorityDesc": "数值越小优先级越高(0 最高)。系统只从最高优先级的供应商中选择。建议:主力=0,备用=1,紧急备份=2",
+        "priorityPlaceholder": "0",
+        "weightLabel": "权重",
+        "weightDesc": "加权随机概率。同优先级内,权重越高被选中概率越大。例如权重 1:2:3 的概率为 16%:33%:50%",
+        "weightPlaceholder": "1",
+        "costMultiplierLabel": "成本倍率",
+        "costMultiplierDesc": "成本计算倍数。官方供应商=1.0,便宜 20%=0.8,贵 20%=1.2(支持最多 4 位小数)",
+        "costMultiplierPlaceholder": "1.0",
+        "providerGroupLabel": "供应商分组",
+        "providerGroupPlaceholder": "例如: premium, economy",
+        "providerGroupDesc": "供应商分组标签。只有用户的 providerGroup 与此值匹配时,该用户才能使用此供应商。示例:设置为 \"premium\" 表示只供 providerGroup=\"premium\" 的用户使用",
+        "rateLimitConfig": "限流配置",
+        "rateLimitConfigSummary": "5h: ${fiveHour}, 周: ${weekly}, 月: ${monthly}, 并发: {concurrent}",
+        "rateLimitConfigNone": "无限制",
+        "limit5hLabel": "5小时消费上限 (USD)",
+        "limitWeeklyLabel": "周消费上限 (USD)",
+        "limitMonthlyLabel": "月消费上限 (USD)",
+        "limitConcurrentLabel": "并发 Session 上限",
+        "limitPlaceholderUnlimited": "留空表示无限制",
+        "limitPlaceholder0": "0 表示无限制",
+        "circuitBreakerConfig": "熔断器配置",
+        "circuitBreakerConfigSummary": "{failureThreshold} 次失败 / {openDuration} 分钟熔断 / {successThreshold} 次成功恢复",
+        "circuitBreakerDesc": "供应商连续失败时自动熔断,避免影响整体服务质量",
+        "failureThreshold": "失败阈值(次)",
+        "failureThresholdPlaceholder": "5",
+        "failureThresholdDesc": "连续失败多少次后触发熔断",
+        "openDuration": "熔断时长(分钟)",
+        "openDurationPlaceholder": "30",
+        "openDurationDesc": "熔断后多久自动进入半开状态",
+        "successThreshold": "恢复阈值(次)",
+        "successThresholdPlaceholder": "2",
+        "successThresholdDesc": "半开状态下成功多少次后完全恢复",
+        "proxyConfig": "代理配置",
+        "proxyConfigSummary": "已配置代理",
+        "proxyConfigSummaryFallback": " (启用降级)",
+        "proxyConfigNone": "未配置",
+        "proxyConfigDesc": "配置代理服务器以改善供应商连接性(支持 HTTP、HTTPS、SOCKS4、SOCKS5)",
+        "proxyAddressLabel": "代理地址",
+        "proxyAddressOptional": "(可选)",
+        "proxyAddressPlaceholder": "例如: http://proxy.example.com:8080 或 socks5://127.0.0.1:1080",
+        "proxyAddressFormats": "支持格式:",
+        "proxyFallbackLabel": "代理失败时降级到直连",
+        "proxyFallbackDesc": "启用后,代理连接失败时自动尝试直接连接供应商",
+        "proxyTestLabel": "连接测试",
+        "proxyTestButton": "测试连接",
+        "proxyTestTesting": "测试中...",
+        "proxyTestSuccess": "连接成功",
+        "proxyTestFailed": "连接失败",
+        "proxyTestDesc": "测试通过配置的代理访问供应商 URL(使用 HEAD 请求,不消耗额度)",
+        "proxyTestFillUrl": "请先填写供应商 URL",
+        "proxyTestResultSuccess": "连接成功 {via}",
+        "proxyTestViaProxy": "(通过代理)",
+        "proxyTestViaDirect": "(直连)",
+        "proxyTestResponseTime": "响应时间: {time}",
+        "proxyTestStatusCode": "| 状态码: {code}",
+        "proxyTestResultFailed": "连接失败",
+        "proxyTestTimeout": "连接超时(5秒)。请检查:\n1. 代理服务器是否可访问\n2. 代理地址和端口是否正确\n3. 代理认证信息是否正确",
+        "proxyTestProxyError": "代理错误: {error}",
+        "proxyTestNetworkError": "网络错误: {error}",
+        "proxyTestResultMessage": "{message}",
+        "proxyTestResultStatusCode": "状态码: {code}",
+        "proxyTestResultResponseTime": "响应时间: {time}ms",
+        "proxyTestResultConnectionMethod": "连接方式: {via}",
+        "proxyTestResultConnectionMethodProxy": "代理",
+        "proxyTestResultConnectionMethodDirect": "直连",
+        "proxyTestResultErrorType": "错误类型: {type}",
+        "codexStrategyConfig": "Codex Instructions 策略",
+        "codexStrategyConfigAuto": "自动 (推荐)",
+        "codexStrategyConfigForce": "强制官方",
+        "codexStrategyConfigKeep": "透传原样",
+        "codexStrategyDesc": "控制如何处理 Codex 请求的 instructions 字段,影响与上游中转站的兼容性",
+        "codexStrategySelect": "策略选择",
+        "codexStrategyAutoLabel": "自动 (推荐)",
+        "codexStrategyAutoDesc": "透传客户端 instructions,400 错误时自动重试官方 prompt",
+        "codexStrategyForceLabel": "强制官方",
+        "codexStrategyForceDesc": "始终使用官方 Codex CLI instructions(约 4000+ 字)",
+        "codexStrategyKeepLabel": "透传原样",
+        "codexStrategyKeepDesc": "始终透传客户端 instructions,不自动重试(适用于宽松中转站)",
+        "codexStrategyHint": "提示: 部分严格的 Codex 中转站(如 88code、foxcode)需要官方 instructions,选择\"自动\"或\"强制官方\"策略",
+        "confirmAdd": "确认添加",
+        "confirmUpdate": "确认更新",
+        "confirmAddPending": "添加中...",
+        "confirmUpdatePending": "更新中...",
+        "deleteButton": "删除",
+        "validUrlRequired": "请输入有效的 API 地址",
+        "filterProvider": "筛选供应商类型",
+        "filterAllProviders": "全部供应商",
+        "searchClear": "清除搜索"
+      },
+      "guide": {
+        "title": "核心原则",
+        "priorityFirst": "1️⃣ 优先级优先:只从最高优先级(数值最小)的供应商中选择",
+        "costOptimize": "2️⃣ 成本优化:同优先级内,成本倍率低的供应商有更高概率",
+        "healthFilter": "3️⃣ 健康过滤:自动跳过熔断或超限的供应商",
+        "sessionReuse": "4️⃣ 会话复用:连续对话复用同一供应商,节省上下文成本",
+        "scenariosTitle": "交互式场景演示",
+        "bestPracticesTitle": "最佳实践建议",
+        "bestPracticesPriority": "• 优先级设置:核心供应商设为 0,备用供应商设为 1-3",
+        "bestPracticesWeight": "• 权重配置:根据供应商容量设置权重(容量大 = 权重高)",
+        "bestPracticesCost": "• 成本倍率:官方倍率为 1.0,自建服务可设置为 0.8-1.2",
+        "bestPracticesLimit": "• 限额设置:根据预算设置 5 小时、7 天、30 天限额",
+        "bestPracticesConcurrent": "• 并发控制:根据供应商 API 限制设置 Session 并发数",
+        "scenario1Title": "优先级分层选择",
+        "scenario1Desc": "系统首先按优先级过滤,只从最高优先级的供应商中选择",
+        "scenario1Step1": "初始状态",
+        "scenario1Step1Desc": "有 4 个已启用的供应商,优先级各不相同",
+        "scenario1Step1Before": "供应商 A (优先级 0), B (优先级 1), C (优先级 0), D (优先级 2)",
+        "scenario1Step1After": "筛选出最高优先级(0)的供应商:A, C",
+        "scenario1Step1Decision": "只从 A 和 C 中选择,B 和 D 被过滤",
+        "scenario1Step2": "成本排序",
+        "scenario1Step2Desc": "在同优先级内,按成本倍率从低到高排序",
+        "scenario1Step2Before": "A (成本 1.0x), C (成本 0.8x)",
+        "scenario1Step2After": "排序后:C (0.8x), A (1.0x)",
+        "scenario1Step2Decision": "成本更低的 C 有更高的被选中概率",
+        "scenario1Step3": "加权随机",
+        "scenario1Step3Desc": "使用权重进行随机选择,权重越高被选中概率越大",
+        "scenario1Step3Before": "C (权重 3), A (权重 1)",
+        "scenario1Step3After": "C 被选中概率 75%, A 被选中概率 25%",
+        "scenario1Step3Decision": "最终随机选择了 C",
+        "scenario2Title": "用户分组过滤",
+        "scenario2Desc": "如果用户指定了供应商组,系统会优先从该组中选择",
+        "scenario2Step1": "检查用户分组",
+        "scenario2Step1Desc": "用户配置了 providerGroup = 'premium'",
+        "scenario2Step1Before": "所有供应商:A (default), B (premium), C (premium), D (economy)",
+        "scenario2Step1After": "过滤出 'premium' 组:B, C",
+        "scenario2Step1Decision": "只从 B 和 C 中选择",
+        "scenario2Step2": "分组降级",
+        "scenario2Step2Desc": "如果用户组内没有可用供应商,降级到所有供应商",
+        "scenario2Step2Before": "用户组 'vip' 内的供应商全部禁用或超限",
+        "scenario2Step2After": "降级到所有启用的供应商:A, B, C, D",
+        "scenario2Step2Decision": "记录警告并从全局供应商池中选择",
+        "scenario3Title": "健康度过滤(熔断器 + 限流)",
+        "scenario3Desc": "系统自动过滤掉熔断或超限的供应商",
+        "scenario3Step1": "熔断器检查",
+        "scenario3Step1Desc": "连续失败 5 次后熔断器打开,60 秒内不可用",
+        "scenario3Step1Before": "供应商 A 连续失败 5 次,熔断器状态:open",
+        "scenario3Step1After": "A 被过滤,剩余:B, C, D",
+        "scenario3Step1Decision": "A 在 60 秒后自动恢复到半开状态",
+        "scenario3Step2": "金额限流",
+        "scenario3Step2Desc": "检查 5 小时、7 天、30 天的消费额度是否超限",
+        "scenario3Step2Before": "供应商 B 的 5 小时限额 $10,已消耗 $9.8",
+        "scenario3Step2After": "B 被过滤(接近限额),剩余:C, D",
+        "scenario3Step2Decision": "5 小时窗口滑动后自动恢复",
+        "scenario3Step3": "并发 Session 限制",
+        "scenario3Step3Desc": "检查当前活跃 Session 数是否超过配置的并发限制",
+        "scenario3Step3Before": "供应商 C 并发限制 2,当前活跃 Session 数:2",
+        "scenario3Step3After": "C 被过滤(已满),剩余:D",
+        "scenario3Step3Decision": "Session 过期(5 分钟)后自动释放",
+        "scenario4Title": "会话复用机制",
+        "scenario4Desc": "连续对话优先使用同一供应商,利用 Claude 的上下文缓存",
+        "scenario4Step1": "检查历史请求",
+        "scenario4Step1Desc": "查询该 API Key 最近 10 秒内使用的供应商",
+        "scenario4Step1Before": "最近一次请求使用了供应商 B",
+        "scenario4Step1After": "检查 B 是否启用且健康",
+        "scenario4Step1Decision": "B 可用,直接复用,跳过随机选择",
+        "scenario4Step2": "复用失效",
+        "scenario4Step2Desc": "如果上次使用的供应商不可用,则重新选择",
+        "scenario4Step2Before": "上次使用的供应商 B 已被禁用或熔断",
+        "scenario4Step2After": "进入正常选择流程",
+        "scenario4Step2Decision": "从其他可用供应商中选择",
+        "step": "步骤",
+        "before": "过滤前:",
+        "after": "过滤后:",
+        "decision": "决策:"
+      }
+    }
+  },
+  "en": {
+    "providers": {
+      "section": {
+        "title": "Provider Management",
+        "description": "Configure upstream provider rate limiting and concurrent session limits. Leave empty for unlimited."
+      },
+      "addProvider": "Add Provider",
+      "editProvider": "Edit Provider",
+      "createProvider": "Add Provider",
+      "updateFailed": "Failed to update provider",
+      "deleteSuccess": "Deleted successfully",
+      "confirmDeleteDesc": "Are you sure you want to delete provider \"{name}\"? This action cannot be undone.",
+      "confirmDeleteProvider": "Confirm Delete Provider?",
+      "confirmDeleteProviderDesc": "Are you sure you want to delete provider \"{name}\"? This action is irreversible.",
+      "noProviders": "No providers configured",
+      "noProvidersDesc": "Add your first API provider",
+      "toggleSuccess": "Provider {status}",
+      "toggleSuccessDesc": "Provider \"{name}\" status updated",
+      "toggleFailed": "Toggle failed",
+      "enabledStatus": "enabled",
+      "disabledStatus": "disabled",
+      "searchResults": "Found {count} matching providers",
+      "searchNoResults": "No matching providers found",
+      "displayCount": "Showing {filtered} / {total} providers",
+      "todayUsage": "Today's Usage",
+      "todayUsageCount": "{count} times",
+      "circuitBroken": "Circuit Broken",
+      "official": "Official",
+      "viewKey": "View Complete API Key",
+      "viewKeyDesc": "Please keep it safe and don't share it with others",
+      "keyLoading": "Loading...",
+      "resetCircuit": "Circuit breaker reset",
+      "resetCircuitDesc": "Provider \"{name}\" circuit breaker status cleared",
+      "resetCircuitFailed": "Failed to reset circuit breaker",
+      "form": {
+        "providerName": "Provider Name",
+        "providerNameRequired": "Provider Name *",
+        "providerNamePlaceholder": "e.g. Zhipu",
+        "apiAddress": "API Address",
+        "apiAddressRequired": "API Address *",
+        "apiAddressPlaceholder": "e.g. https://open.bigmodel.cn/api/anthropic",
+        "apiKeyRequired": "API Key *",
+        "apiKeyLeaveEmpty": "(Leave empty to keep unchanged)",
+        "apiKeyLeaveEmptyDesc": "Leave empty to keep existing key",
+        "apiKeyCurrent": "Current key:",
+        "websiteUrl": "Provider Website URL",
+        "websiteUrlPlaceholder": "https://example.com",
+        "websiteUrlDesc": "Provider website URL for quick access",
+        "websiteUrlInvalid": "Please enter a valid provider website URL",
+        "expandAll": "Expand All Advanced Configuration",
+        "collapseAll": "Collapse All Advanced Configuration",
+        "routingConfig": "Routing Configuration",
+        "routingConfigSummary": "{models} model whitelist, {redirects} redirects",
+        "routingConfigNone": "Not configured",
+        "providerTypeDesc": "Select the API format type for the provider.",
+        "providerTypeDisabledNote": "Note: Gemini CLI and OpenAI Compatible types are under development",
+        "modelRedirectsLabel": "Model Redirects Configuration",
+        "modelRedirectsOptional": "(Optional)",
+        "modelRedirectsDesc": "Redirect Claude Code client model requests (e.g. claude-sonnet-4.5) to upstream provider supported models (e.g. glm-4.6, gemini-pro). For cost optimization or third-party AI integration.",
+        "modelRedirectsEmpty": "No redirect rules yet. System will auto-rewrite model names after adding rules.",
+        "modelRedirectsCurrentRules": "Current Rules ({count})",
+        "modelRedirectsAddNew": "Add New Rule",
+        "modelRedirectsSourceModel": "User Requested Model",
+        "modelRedirectsSourcePlaceholder": "e.g. claude-sonnet-4-5-20250929",
+        "modelRedirectsTargetModel": "Actual Forwarded Model",
+        "modelRedirectsTargetPlaceholder": "e.g. glm-4.6",
+        "modelRedirectsSourceRequired": "Source model name cannot be empty",
+        "modelRedirectsTargetRequired": "Target model name cannot be empty",
+        "modelRedirectsExists": "Model \"{model}\" already has a redirect rule",
+        "joinClaudePool": "Join Claude Scheduling Pool",
+        "joinClaudePoolDesc": "When enabled, this provider will participate in load balancing with Claude type providers",
+        "joinClaudePoolHelp": "Only available when model redirect config contains mappings to claude-* models. When enabled, this provider will also participate in scheduling when users request claude-* models.",
+        "modelWhitelist": "Model Whitelist",
+        "modelWhitelistDesc": "Limit models this provider can handle. By default, provider can handle all models of its type.",
+        "modelWhitelistLabel": "Allowed Models",
+        "modelWhitelistSelected": "Selected {count} models",
+        "modelWhitelistLoading": "Loading...",
+        "modelWhitelistNotFound": "Model not found",
+        "modelWhitelistSearchPlaceholder": "Search model name...",
+        "modelWhitelistSelectAll": "Select All ({count})",
+        "modelWhitelistClear": "Clear",
+        "modelWhitelistManualAdd": "Manually Add Model",
+        "modelWhitelistManualPlaceholder": "Enter model name (e.g. gpt-5-turbo)",
+        "modelWhitelistManualDesc": "Support adding any model name (not limited to price table)",
+        "modelWhitelistAllowAll": "Allow all {type} models",
+        "modelWhitelistAllowAllClause": "Allow all Claude models",
+        "modelWhitelistAllowAllOpenAI": "Allow all OpenAI models",
+        "modelWhitelistSelectedOnly": "Only allow selected {count} models. Requests for other models won't be routed to this provider.",
+        "scheduleParams": "Scheduling Parameters",
+        "priorityLabel": "Priority",
+        "priorityDesc": "Lower number = higher priority (0 is highest). System only selects from highest priority providers. Recommendation: Main=0, Backup=1, Emergency=2",
+        "priorityPlaceholder": "0",
+        "weightLabel": "Weight",
+        "weightDesc": "Weighted random probability. Within same priority, higher weight = higher selection probability. E.g. weights 1:2:3 = probabilities 16%:33%:50%",
+        "weightPlaceholder": "1",
+        "costMultiplierLabel": "Cost Multiplier",
+        "costMultiplierDesc": "Cost calculation multiplier. Official=1.0, 20% cheaper=0.8, 20% more expensive=1.2 (up to 4 decimal places)",
+        "costMultiplierPlaceholder": "1.0",
+        "providerGroupLabel": "Provider Group",
+        "providerGroupPlaceholder": "e.g. premium, economy",
+        "providerGroupDesc": "Provider group tag. Only users with matching providerGroup can use this provider. Example: Set to \"premium\" to allow only providerGroup=\"premium\" users",
+        "rateLimitConfig": "Rate Limit Configuration",
+        "rateLimitConfigSummary": "5h: ${fiveHour}, Weekly: ${weekly}, Monthly: ${monthly}, Concurrent: {concurrent}",
+        "rateLimitConfigNone": "Unlimited",
+        "limit5hLabel": "5-Hour Spending Limit (USD)",
+        "limitWeeklyLabel": "Weekly Spending Limit (USD)",
+        "limitMonthlyLabel": "Monthly Spending Limit (USD)",
+        "limitConcurrentLabel": "Concurrent Session Limit",
+        "limitPlaceholderUnlimited": "Leave empty for unlimited",
+        "limitPlaceholder0": "0 means unlimited",
+        "circuitBreakerConfig": "Circuit Breaker Configuration",
+        "circuitBreakerConfigSummary": "{failureThreshold} failures / {openDuration} min circuit break / {successThreshold} successes to recover",
+        "circuitBreakerDesc": "Auto circuit break on consecutive failures to avoid overall service quality impact",
+        "failureThreshold": "Failure Threshold (times)",
+        "failureThresholdPlaceholder": "5",
+        "failureThresholdDesc": "How many consecutive failures trigger circuit break",
+        "openDuration": "Circuit Break Duration (minutes)",
+        "openDurationPlaceholder": "30",
+        "openDurationDesc": "How long before auto entering half-open state",
+        "successThreshold": "Recovery Threshold (times)",
+        "successThresholdPlaceholder": "2",
+        "successThresholdDesc": "How many successes in half-open state to fully recover",
+        "proxyConfig": "Proxy Configuration",
+        "proxyConfigSummary": "Proxy configured",
+        "proxyConfigSummaryFallback": " (fallback enabled)",
+        "proxyConfigNone": "Not configured",
+        "proxyConfigDesc": "Configure proxy server to improve provider connectivity (supports HTTP, HTTPS, SOCKS4, SOCKS5)",
+        "proxyAddressLabel": "Proxy Address",
+        "proxyAddressOptional": "(Optional)",
+        "proxyAddressPlaceholder": "e.g. http://proxy.example.com:8080 or socks5://127.0.0.1:1080",
+        "proxyAddressFormats": "Supported formats:",
+        "proxyFallbackLabel": "Fallback to direct on proxy failure",
+        "proxyFallbackDesc": "When enabled, auto try direct connection on proxy failure",
+        "proxyTestLabel": "Connection Test",
+        "proxyTestButton": "Test Connection",
+        "proxyTestTesting": "Testing...",
+        "proxyTestSuccess": "Connection Successful",
+        "proxyTestFailed": "Connection Failed",
+        "proxyTestDesc": "Test provider URL access via configured proxy (uses HEAD request, no quota consumption)",
+        "proxyTestFillUrl": "Please fill in provider URL first",
+        "proxyTestResultSuccess": "Connection successful {via}",
+        "proxyTestViaProxy": "(via proxy)",
+        "proxyTestViaDirect": "(direct)",
+        "proxyTestResponseTime": "Response time: {time}",
+        "proxyTestStatusCode": "| Status code: {code}",
+        "proxyTestResultFailed": "Connection failed",
+        "proxyTestTimeout": "Connection timeout (5s). Please check:\n1. Is proxy server accessible\n2. Are proxy address and port correct\n3. Are proxy credentials correct",
+        "proxyTestProxyError": "Proxy error: {error}",
+        "proxyTestNetworkError": "Network error: {error}",
+        "proxyTestResultMessage": "{message}",
+        "proxyTestResultStatusCode": "Status code: {code}",
+        "proxyTestResultResponseTime": "Response time: {time}ms",
+        "proxyTestResultConnectionMethod": "Connection method: {via}",
+        "proxyTestResultConnectionMethodProxy": "Proxy",
+        "proxyTestResultConnectionMethodDirect": "Direct",
+        "proxyTestResultErrorType": "Error type: {type}",
+        "codexStrategyConfig": "Codex Instructions Strategy",
+        "codexStrategyConfigAuto": "Auto (Recommended)",
+        "codexStrategyConfigForce": "Force Official",
+        "codexStrategyConfigKeep": "Keep Original",
+        "codexStrategyDesc": "Control how to handle Codex request instructions field, affects upstream gateway compatibility",
+        "codexStrategySelect": "Strategy Selection",
+        "codexStrategyAutoLabel": "Auto (Recommended)",
+        "codexStrategyAutoDesc": "Pass through client instructions, auto retry with official prompt on 400 error",
+        "codexStrategyForceLabel": "Force Official",
+        "codexStrategyForceDesc": "Always use official Codex CLI instructions (~4000+ chars)",
+        "codexStrategyKeepLabel": "Keep Original",
+        "codexStrategyKeepDesc": "Always pass through client instructions, no auto retry (for lenient gateways)",
+        "codexStrategyHint": "Hint: Some strict Codex gateways (e.g. 88code, foxcode) require official instructions. Choose \"Auto\" or \"Force Official\" strategy",
+        "confirmAdd": "Confirm Add",
+        "confirmUpdate": "Confirm Update",
+        "confirmAddPending": "Adding...",
+        "confirmUpdatePending": "Updating...",
+        "deleteButton": "Delete",
+        "validUrlRequired": "Please enter a valid API address",
+        "filterProvider": "Filter by Provider Type",
+        "filterAllProviders": "All Providers",
+        "searchClear": "Clear search"
+      },
+      "guide": {
+        "title": "Core Principles",
+        "priorityFirst": "1️⃣ Priority First: Select only from highest priority (lowest number) providers",
+        "costOptimize": "2️⃣ Cost Optimization: Within same priority, lower cost multiplier has higher probability",
+        "healthFilter": "3️⃣ Health Filtering: Auto skip circuit-broken or over-limit providers",
+        "sessionReuse": "4️⃣ Session Reuse: Consecutive chats reuse same provider, saving context costs",
+        "scenariosTitle": "Interactive Scenario Demos",
+        "bestPracticesTitle": "Best Practices",
+        "bestPracticesPriority": "• Priority Settings: Core providers=0, Backup=1-3",
+        "bestPracticesWeight": "• Weight Config: Set weight by capacity (higher capacity = higher weight)",
+        "bestPracticesCost": "• Cost Multiplier: Official=1.0, Self-hosted can be 0.8-1.2",
+        "bestPracticesLimit": "• Limit Settings: Set 5h, 7d, 30d limits based on budget",
+        "bestPracticesConcurrent": "• Concurrent Control: Set session concurrency by provider API limits",
+        "scenario1Title": "Priority Layering",
+        "scenario1Desc": "System first filters by priority, selecting only from highest priority providers",
+        "scenario1Step1": "Initial State",
+        "scenario1Step1Desc": "4 enabled providers with different priorities",
+        "scenario1Step1Before": "Provider A (priority 0), B (priority 1), C (priority 0), D (priority 2)",
+        "scenario1Step1After": "Filtered to highest priority (0) providers: A, C",
+        "scenario1Step1Decision": "Select only from A and C, B and D filtered out",
+        "scenario1Step2": "Cost Sorting",
+        "scenario1Step2Desc": "Within same priority, sort by cost multiplier low to high",
+        "scenario1Step2Before": "A (cost 1.0x), C (cost 0.8x)",
+        "scenario1Step2After": "After sorting: C (0.8x), A (1.0x)",
+        "scenario1Step2Decision": "Lower cost C has higher selection probability",
+        "scenario1Step3": "Weighted Random",
+        "scenario1Step3Desc": "Use weight for random selection, higher weight = higher probability",
+        "scenario1Step3Before": "C (weight 3), A (weight 1)",
+        "scenario1Step3After": "C has 75% probability, A has 25%",
+        "scenario1Step3Decision": "Finally randomly selected C",
+        "scenario2Title": "User Group Filtering",
+        "scenario2Desc": "If user has provider group specified, system prioritizes selection from that group",
+        "scenario2Step1": "Check User Group",
+        "scenario2Step1Desc": "User configured providerGroup = 'premium'",
+        "scenario2Step1Before": "All providers: A (default), B (premium), C (premium), D (economy)",
+        "scenario2Step1After": "Filtered to 'premium' group: B, C",
+        "scenario2Step1Decision": "Select only from B and C",
+        "scenario2Step2": "Group Fallback",
+        "scenario2Step2Desc": "If no available providers in user group, fallback to all providers",
+        "scenario2Step2Before": "All providers in user group 'vip' disabled or over limit",
+        "scenario2Step2After": "Fallback to all enabled providers: A, B, C, D",
+        "scenario2Step2Decision": "Log warning and select from global provider pool",
+        "scenario3Title": "Health Filtering (Circuit Breaker + Rate Limit)",
+        "scenario3Desc": "System auto filters circuit-broken or over-limit providers",
+        "scenario3Step1": "Circuit Breaker Check",
+        "scenario3Step1Desc": "Circuit breaker opens after 5 consecutive failures, unavailable for 60s",
+        "scenario3Step1Before": "Provider A failed 5 times, circuit breaker: open",
+        "scenario3Step1After": "A filtered, remaining: B, C, D",
+        "scenario3Step1Decision": "A auto recovers to half-open after 60s",
+        "scenario3Step2": "Amount Rate Limit",
+        "scenario3Step2Desc": "Check if spending exceeds limits (5h, 7d, 30d)",
+        "scenario3Step2Before": "Provider B 5h limit $10, consumed $9.8",
+        "scenario3Step2After": "B filtered (near limit), remaining: C, D",
+        "scenario3Step2Decision": "Auto recovery after 5h sliding window",
+        "scenario3Step3": "Concurrent Session Limit",
+        "scenario3Step3Desc": "Check if active session count exceeds configured concurrent limit",
+        "scenario3Step3Before": "Provider C concurrent limit 2, currently 2 active sessions",
+        "scenario3Step3After": "C filtered (full), remaining: D",
+        "scenario3Step3Decision": "Auto release after session expiry (5 min)",
+        "scenario4Title": "Session Reuse Mechanism",
+        "scenario4Desc": "Consecutive chats prioritize using same provider, leveraging Claude context cache",
+        "scenario4Step1": "Check Request History",
+        "scenario4Step1Desc": "Query providers used by this API Key in last 10 seconds",
+        "scenario4Step1Before": "Last request used provider B",
+        "scenario4Step1After": "Check if B is enabled and healthy",
+        "scenario4Step1Decision": "B available, reuse directly, skip random selection",
+        "scenario4Step2": "Reuse Invalidation",
+        "scenario4Step2Desc": "If last used provider unavailable, reselect",
+        "scenario4Step2Before": "Last used provider B disabled or circuit-broken",
+        "scenario4Step2After": "Enter normal selection flow",
+        "scenario4Step2Decision": "Select from other available providers",
+        "step": "Step",
+        "before": "Before:",
+        "after": "After:",
+        "decision": "Decision:"
+      }
+    }
+  }
+}

+ 2 - 0
messages/ru/index.ts

@@ -10,6 +10,7 @@ import settings from "./settings.json";
 import ui from "./ui.json";
 import usage from "./usage.json";
 import validation from "./validation.json";
+import internal from "./internal.json";
 
 export default {
   auth,
@@ -24,4 +25,5 @@ export default {
   ui,
   usage,
   validation,
+  internal,
 };

+ 83 - 0
messages/ru/internal.json

@@ -0,0 +1,83 @@
+{
+  "dataGenerator": {
+    "tabs": {
+      "usage": "Генерация данных использования",
+      "userBreakdown": "Детализация по пользователям"
+    },
+    "actions": {
+      "reconfigure": "Перенастроить параметры",
+      "generate": "Генерировать данные",
+      "exportScreenshot": "Экспорт скриншота",
+      "exportPDF": "Экспорт PDF"
+    },
+    "params": {
+      "title": "Параметры генерации",
+      "description": "Настройте параметры для создания симулированных данных журнала",
+      "startDate": "Время начала",
+      "endDate": "Время окончания",
+      "totalCostCny": "Общая сумма (CNY)",
+      "totalRecords": "Всего записей",
+      "models": "Включить модели (через запятую)",
+      "userIds": "ID пользователей (через запятую)",
+      "providerIds": "ID провайдеров (через запятую)",
+      "serviceName": "Название сервиса",
+      "required": "*",
+      "placeholders": {
+        "totalCostCny": "напр., 1000",
+        "totalRecords": "напр., 500 (оставьте пустым для расчета по сумме)",
+        "models": "напр., claude-3-5-sonnet,gpt-4 (пусто = все)",
+        "userIds": "напр., 1,2,3 (пусто = все)",
+        "providerIds": "напр., 1,2 (пусто = все)",
+        "serviceName": "Сервис вывода AI моделей"
+      }
+    },
+    "summary": {
+      "totalRecords": "Всего записей",
+      "totalCost": "Общая стоимость",
+      "totalCostCny": "Общая стоимость (CNY)",
+      "totalTokens": "Всего токенов",
+      "timeRange": "Временной диапазон",
+      "to": "до",
+      "uniqueUsers": "Всего пользователей",
+      "totalCalls": "Всего вызовов"
+    },
+    "table": {
+      "usageLogs": {
+        "title": "Журнал использования",
+        "description": "Всего {count} записей",
+        "columns": {
+          "time": "Время",
+          "user": "Пользователь",
+          "key": "Ключ",
+          "provider": "Провайдер",
+          "model": "Модель",
+          "input": "Ввод",
+          "output": "Вывод",
+          "cacheWrite": "Запись кэша",
+          "cacheRead": "Чтение кэша",
+          "cost": "Стоимость",
+          "duration": "Длительность",
+          "status": "Статус"
+        }
+      },
+      "userBreakdown": {
+        "title": "Детали использования пользователей",
+        "description": "Всего {count} записей",
+        "collapseByUser": "Свернуть по пользователям",
+        "columns": {
+          "userName": "Имя пользователя",
+          "key": "Ключ",
+          "serviceModel": "Модель сервиса",
+          "totalCalls": "Всего вызовов",
+          "totalCost": "Общая стоимость"
+        }
+      }
+    },
+    "status": {
+      "success": "Успешно"
+    },
+    "errors": {
+      "failed": "Не удалось сгенерировать журналы"
+    }
+  }
+}

+ 180 - 30
messages/ru/quota.json

@@ -1,55 +1,205 @@
 {
   "header": {
-    "title": "Квота пользователя",
+    "title": "用户配额",
     "role": {
-      "admin": "Администратор",
-      "user": "Пользователь"
+      "admin": "管理员",
+      "user": "用户"
     },
-    "keysCountSuffix": "ключей",
-    "rpm": "Лимит запросов",
-    "todayCost": "Расход за день",
-    "exceededNotice": "Квота превышена, пожалуйста, обратитесь к администратору"
+    "keysCountSuffix": "个密钥",
+    "rpm": "请求限流",
+    "todayCost": "今日消费",
+    "exceededNotice": "已超过限额,请联系管理员处理"
   },
   "countdown": {
-    "reset": "Отсчет до сброса"
+    "reset": "重置倒计时"
   },
   "windowType": {
     "5h": {
-      "label": "5-часовое скользящее окно",
-      "description": "Отслеживает расход за последние 5 часов с ежечасным обновлением"
+      "label": "5小时滚动",
+      "description": "统计过去5小时内的消费,每小时滚动更新"
     },
     "weekly": {
-      "label": "Еженедельный сброс",
-      "description": "Календарная неделя (сброс в понедельник 00:00), учитывает всю неделю"
+      "label": "每周重置",
+      "description": "自然周制(周一00:00重置),整周统计"
     },
     "monthly": {
-      "label": "Ежемесячный сброс",
-      "description": "Календарный месяц (сброс 1-го числа 00:00), учитывает весь месяц"
+      "label": "每月重置",
+      "description": "自然月制(每月1日00:00重置),整月统计"
     },
     "daily": {
-      "label": "Ежедневный сброс",
-      "description": "Календарный день (сброс ежедневно 00:00), учитывает по дням"
+      "label": "每日重置",
+      "description": "自然日制(每日00:00重置),按日统计"
     }
   },
   "toolbar": {
-    "searchPlaceholder": "Поиск по имени пользователя или ключу",
-    "filter": "Фильтр",
-    "sort": "Сортировка",
-    "refresh": "Обновить",
-    "autoRefresh": "Автоматическое обновление",
+    "searchPlaceholder": "搜索用户名或密钥",
+    "filter": "筛选",
+    "sort": "排序",
+    "refresh": "刷新",
+    "autoRefresh": "自动刷新",
     "filterOptions": {
-      "all": "Все",
-      "warning": "Предупреждение",
-      "exceeded": "Превышено"
+      "all": "全部",
+      "warning": "预警",
+      "exceeded": "已超限"
     },
     "sortOptions": {
-      "name": "По названию",
-      "usage": "По использованию"
+      "name": "按名称",
+      "usage": "按使用率"
     },
     "interval": {
-      "10s": "10 сек",
-      "30s": "30 сек",
-      "60s": "60 сек"
+      "10s": "10秒",
+      "30s": "30秒",
+      "60s": "60秒"
+    }
+  },
+  "layout": {
+    "title": "Управление квотами",
+    "description": "Просмотр и управление использованием квот на всех уровнях",
+    "tabs": {
+      "users": "Квоты пользователей",
+      "keys": "Квоты ключей",
+      "providers": "Квоты провайдеров"
+    }
+  },
+  "users": {
+    "title": "Статистика квот пользователей",
+    "totalCount": "Всего пользователей: {count}",
+    "noNote": "Нет заметок",
+    "rpm": {
+      "label": "Квота RPM",
+      "description": "Запросов в минуту"
+    },
+    "dailyCost": {
+      "label": "Дневные расходы",
+      "resetAt": "Сброс в"
+    },
+    "noQuotaData": "Невозможно получить информацию о квоте",
+    "noMatches": "Совпадающие пользователи не найдены",
+    "noData": "Нет данных пользователей",
+    "sort": {
+      "name": "По имени",
+      "usage": "По использованию"
+    },
+    "filter": {
+      "all": "Все",
+      "warning": "Близко к лимиту (>60%)",
+      "exceeded": "Превышено (≥100%)"
+    }
+  },
+  "providers": {
+    "title": "Статистика квот провайдеров",
+    "totalCount": "Всего провайдеров: {count}",
+    "filterCount": "Показано {filtered} / {total} провайдеров",
+    "status": {
+      "enabled": "Включено",
+      "disabled": "Отключено"
+    },
+    "card": {
+      "priority": "Приоритет",
+      "weight": "Вес"
+    },
+    "cost5h": {
+      "label": "Расходы за 5 часов"
+    },
+    "costWeekly": {
+      "label": "Недельные расходы",
+      "resetAt": "Сброс в"
+    },
+    "costMonthly": {
+      "label": "Месячные расходы",
+      "resetAt": "Сброс в"
+    },
+    "concurrentSessions": {
+      "label": "Одновременные сессии"
+    },
+    "noQuotaSet": "Квота не установлена",
+    "noQuotaData": "Невозможно получить информацию о квоте",
+    "noMatches": "Совпадающие провайдеры не найдены",
+    "unlimitedSection": "Провайдеры без квоты ({count})"
+  },
+  "keys": {
+    "title": "Статистика квот ключей",
+    "totalCount": "Всего {users} пользователей, {keys} ключей",
+    "searchPlaceholder": "Поиск пользователя или ключа...",
+    "filterLabel": "Условия фильтра",
+    "filterCount": "Показано {users} пользователей, {keys} ключей",
+    "filter": {
+      "all": "Все ключи",
+      "keyQuota": "Только квоты ключей",
+      "userQuotaOnly": "Только квоты пользователей",
+      "warning": "Предупреждение (≥60%)",
+      "exceeded": "Превышено (≥100%)"
+    },
+    "table": {
+      "keyName": "Имя ключа",
+      "quotaType": "Тип квоты",
+      "cost5h": "Квота за 5 часов",
+      "costWeekly": "Недельная квота",
+      "costMonthly": "Месячная квота",
+      "concurrentSessions": "Лимит одновременных",
+      "status": "Статус",
+      "actions": "Действия"
+    },
+    "quotaType": {
+      "independent": "Независимая квота",
+      "inherited": "Унаследовано от пользователя"
+    },
+    "status": {
+      "disabled": "Отключено",
+      "restricted": "Ограничено",
+      "normal": "Нормально"
+    },
+    "noMatches": "Совпадающие пользователи или ключи не найдены",
+    "editDialog": {
+      "title": "Установить квоту ключа",
+      "description": "密钥: {keyName} ({userName})",
+      "cost5h": {
+        "label": "5小时限额(USD)",
+        "placeholder": "不限制",
+        "current": "当前已用: {currency}{current} / {currency}{limit}"
+      },
+      "costWeekly": {
+        "label": "周限额(USD)",
+        "placeholder": "不限制",
+        "current": "当前已用: {currency}{current} / {currency}{limit}"
+      },
+      "costMonthly": {
+        "label": "月限额(USD)",
+        "placeholder": "不限制",
+        "current": "当前已用: {currency}{current} / {currency}{limit}"
+      },
+      "concurrentSessions": {
+        "label": "并发 Session 限额",
+        "placeholder": "0 = 不限制",
+        "current": "当前并发: {current} / {limit}"
+      },
+      "clearAll": "清除所有限额",
+      "save": "Сохранить",
+      "setQuota": "设置限额",
+      "success": "Квота успешно установлена",
+      "clearSuccess": "限额已清除",
+      "error": "设置失败",
+      "clearError": "清除失败",
+      "retryError": "设置失败,请稍后重试"
+    },
+    "editUserDialog": {
+      "title": "Установить квоту пользователя",
+      "description": "用户: {userName}",
+      "rpm": {
+        "label": "每分钟请求数 (RPM)",
+        "placeholder": "60",
+        "current": "当前: {current} / {limit} 请求/分钟"
+      },
+      "dailyQuota": {
+        "label": "每日消费限额(USD)",
+        "placeholder": "100",
+        "current": "今日已用: {currency}{current} / {currency}{limit}"
+      },
+      "save": "Сохранить",
+      "editQuota": "编辑限额",
+      "success": "用户限额设置成功",
+      "error": "设置失败",
+      "retryError": "设置失败,请稍后重试"
     }
   }
-}
+}

+ 115 - 9
messages/ru/settings.json

@@ -201,24 +201,66 @@
     "description": "Управление конфигурацией платформы и ценами моделей",
     "subtitle": "Цены моделей",
     "subtitleDesc": "Управление ценами AI моделей",
-    "search": "Поиск по названию модели...",
-    "sync": "Синхронизировать цены LiteLLM",
+    "search": {
+      "placeholder": "Поиск названия модели...",
+      "totalModels": "Всего {total} цен моделей",
+      "searchResults": "(Результаты поиска: {count})",
+      "lastUpdated": "Последнее обновление:"
+    },
+    "sync": {
+      "button": "Синхронизировать цены LiteLLM",
+      "syncing": "Синхронизация...",
+      "successWithChanges": "Синхронизация успешна: Добавлено {added}, Обновлено {updated}, Без изменений {unchanged}",
+      "successNoChanges": "Все {unchanged} цены моделей актуальны",
+      "noModels": "Поддерживаемые цены моделей не найдены",
+      "failed": "Синхронизация не удалась",
+      "failedError": "Синхронизация не удалась, попробуйте еще раз",
+      "failedNoResult": "Синхронизация успешна, но результат не возвращен",
+      "partialFailure": "Не удалось обработать {failed} моделей"
+    },
     "syncing": "Синхронизация...",
     "syncSuccess": "Прайс-лист обновлен успешно",
     "syncFailed": "Ошибка синхронизации",
     "syncFailedError": "Ошибка синхронизации:",
     "syncNoResult": "Прайс-лист обновлен но результат не возвращен",
-    "upload": "Обновить прайс-лист",
+    "upload": {
+      "button": "Обновить таблицу цен",
+      "updating": "Обновление...",
+      "dialogTitle": "Обновить таблицу цен модели",
+      "dialogDescription": "Выберите JSON-файл с данными о ценах модели для обновления таблицы цен",
+      "selectFile": "Нажмите, чтобы выбрать JSON-файл или перетащите его сюда",
+      "fileSizeLimit": "Размер файла не должен превышать 10 МБ",
+      "selectFileButton": "Выбрать файл",
+      "invalidFileType": "Пожалуйста, выберите JSON-файл",
+      "fileTooLarge": "Размер файла не должен превышать 10 МБ",
+      "success": "Таблица цен успешно обновлена",
+      "failed": "Обновление не удалось, попробуйте еще раз",
+      "failedError": "Обновление не удалось:",
+      "noResult": "Таблица цен успешно обновлена, но результат не возвращен",
+      "updateInProgress": "Обновление цен модели...",
+      "enterDashboard": "Перейти в панель управления",
+      "completed": "Завершено"
+    },
     "uploadSuccess": "Прайс-лист обновлен успешно",
     "uploadFailed": "Ошибка получения данных о ценах:",
     "noData": "Система имеет встроенный прайс-лист. Используйте кнопки выше для синхронизации.",
     "noModels": "Цены моделей не найдены",
     "table": {
-      "model": "Модель",
-      "inputPrice": "Цена входа",
-      "outputPrice": "Цена выхода",
-      "cachePrice": "Цена кэша",
-      "updatedAt": "Обновлено"
+      "modelName": "Название модели",
+      "type": "Тип",
+      "provider": "Провайдер",
+      "inputPrice": "Цена ввода ($/M)",
+      "outputPrice": "Цена вывода ($/M)",
+      "updatedAt": "Обновлено",
+      "typeChat": "Чат",
+      "typeImageGen": "Генерация изображений",
+      "typeCompletion": "Завершение",
+      "typeUnknown": "Неизвестно",
+      "imagePrice": "${price}/изобр",
+      "loading": "Загрузка...",
+      "noResults": "Подходящие модели не найдены",
+      "noData": "Данные о ценах отсутствуют",
+      "noDataHint": "Встроенная таблица цен доступна. Используйте кнопки выше для синхронизации или обновления"
     },
     "dialog": {
       "title": "Обновить прайс-лист",
@@ -230,6 +272,33 @@
       "uploading": "Загрузка...",
       "readError": "Ошибка получения ключа",
       "getError": "Ошибка получения ключа"
+    },
+    "section": {
+      "title": "Цены модели",
+      "description": "Управление конфигурацией цен AI моделей"
+    },
+    "pagination": {
+      "perPage": "На странице:",
+      "showing": "Показано {start} - {end} из {total} записей",
+      "previous": "Предыдущая",
+      "next": "Следующая"
+    },
+    "uploadDialog": {
+      "notes": {
+        "litellm": "• Встроенная таблица цен LiteLLM. Используйте кнопку 'Синхронизировать цены LiteLLM' слева для обновления",
+        "manual": "• Вы также можете загрузить вручную",
+        "link": "последнюю таблицу цен",
+        "linkAnd": "и обновить через эту кнопку",
+        "support": "• Поддерживает модели Claude и OpenAI (префикс claude-, gpt-, o1-, o3-)"
+      },
+      "result": {
+        "total": "Всего обработано",
+        "added": "Добавлено моделей ({count})",
+        "updated": "Обновлено моделей ({count})",
+        "unchanged": "Без изменений ({count})",
+        "failed": "Не удалось ({count})",
+        "showMore": "и еще {count}"
+      }
     }
   },
   "sensitiveWords": {
@@ -276,6 +345,43 @@
       "totalChecks": "Всего проверок",
       "cacheSize": "Размер кэша",
       "lastUpdated": "Последнее обновление"
+    },
+    "section": {
+      "title": "Список чувствительных слов",
+      "description": "Запросы, заблокированные чувствительными словами, не будут пересылаться и не будут тарифицироваться. Поддерживает частичное совпадение, точное совпадение и регулярные выражения."
+    },
+    "refreshCacheSuccess": "Кэш успешно обновлен, загружено {count} чувствительных слов",
+    "refreshCacheFailed": "Не удалось обновить кэш",
+    "cacheStats": "Статистика кэша: Содержит({containsCount}) Точное({exactCount}) Регулярное({regexCount})",
+    "emptyState": "Пока нет чувствительных слов. Нажмите 'Добавить чувствительное слово' в правом верхнем углу для начала настройки.",
+    "confirmDelete": "Вы уверены, что хотите удалить чувствительное слово \"{word}\"?",
+    "dialog": {
+      "addTitle": "Добавить чувствительное слово",
+      "addDescription": "Настройте правила фильтрации чувствительных слов. Совпадающие запросы не будут пересылаться.",
+      "editTitle": "Редактировать чувствительное слово",
+      "editDescription": "Изменить конфигурацию чувствительного слова. Изменения автоматически обновят кэш.",
+      "wordLabel": "Чувствительное слово *",
+      "wordPlaceholder": "Введите чувствительное слово...",
+      "wordRequired": "Пожалуйста, введите чувствительное слово",
+      "matchTypeLabel": "Тип совпадения *",
+      "matchTypeContains": "Частичное совпадение - Блокировать, если текст содержит это слово",
+      "matchTypeExact": "Точное совпадение - Блокировать только при точном совпадении",
+      "matchTypeRegex": "Регулярное выражение - Поддержка сложного сопоставления шаблонов",
+      "descriptionLabel": "Описание",
+      "descriptionPlaceholder": "Необязательно: Добавить описание...",
+      "creating": "Создание...",
+      "saving": "Сохранение..."
+    },
+    "table": {
+      "word": "Чувствительное слово",
+      "matchType": "Тип совпадения",
+      "matchTypeContains": "Частичное",
+      "matchTypeExact": "Точное",
+      "matchTypeRegex": "Регулярное",
+      "description": "Описание",
+      "status": "Статус",
+      "createdAt": "Создано",
+      "actions": "Действия"
     }
   },
   "logs": {
@@ -433,4 +539,4 @@
     "loadFailed": "Ошибка загрузки параметров уведомлений",
     "unknownError": "Ошибка при выполнении операции"
   }
-}
+}

+ 19 - 1
messages/ru/usage.json

@@ -391,6 +391,23 @@
           "Начните использовать!"
         ]
       }
+    },
+
+    "startup": {
+      "title": "Запуск droid",
+      "description": "Выполните следующую команду в каталоге вашего проекта:",
+      "initNote": "При первом запуске droid выполнит первоначальную настройку."
+    },
+
+    "commonIssues": {
+      "title": "Часто задаваемые вопросы",
+      "commandNotFound": "1. Команда не найдена",
+      "commandNotFoundWindows": [
+        "Убедитесь, что глобальный путь npm (обычно C:\\Users\\your-username\\AppData\\Roaming\\npm) добавлен в системный PATH",
+        "Перезапустите окно PowerShell"
+      ],
+      "commandNotFoundUnix": "Проверьте глобальный путь установки npm и добавьте его в PATH (если еще не добавлен)",
+      "updateCli": "2. Обновление droid"
     }
   },
 
@@ -448,6 +465,7 @@
 
   "ui": {
     "mainContent": "Содержимое документации",
-    "main": "main"
+    "main": "main",
+    "currentSiteAddress": "Текущий адрес сайта"
   }
 }

+ 215 - 20
messages/zh-CN/dashboard.json

@@ -55,18 +55,110 @@
       "provider": "供应商",
       "model": "模型",
       "status": "状态",
-      "timeRange": "时间范围"
+      "timeRange": "时间范围",
+      "startTime": "开始时间",
+      "endTime": "结束时间",
+      "allUsers": "全部用户",
+      "allKeys": "全部密钥",
+      "selectUserFirst": "请先选择用户",
+      "allProviders": "全部供应商",
+      "allModels": "全部模型",
+      "allStatusCodes": "全部状态码",
+      "apiKey": "API 密钥",
+      "statusCode": "状态码",
+      "apply": "应用筛选",
+      "reset": "重置"
     },
     "columns": {
       "time": "时间",
       "user": "用户",
+      "key": "密钥",
       "provider": "供应商",
       "model": "模型",
-      "inputTokens": "输入 Tokens",
-      "outputTokens": "输出 Tokens",
+      "inputTokens": "输入",
+      "outputTokens": "输出",
+      "cacheWrite": "缓存写入",
+      "cacheRead": "缓存读取",
       "cost": "成本",
-      "status": "状态",
-      "duration": "耗时"
+      "duration": "耗时",
+      "status": "状态"
+    },
+    "stats": {
+      "totalAmount": "总消耗金额",
+      "totalTokens": "总 Token 数",
+      "cacheTokens": "缓存 Token",
+      "input": "输入",
+      "output": "输出",
+      "write": "写入",
+      "read": "读取"
+    },
+    "table": {
+      "noData": "暂无数据",
+      "pagination": "共 {total} 条记录,第 {page} / {totalPages} 页",
+      "prevPage": "上一页",
+      "nextPage": "下一页",
+      "blocked": "被拦截",
+      "times": "次"
+    },
+    "actions": {
+      "refresh": "刷新",
+      "refreshing": "刷新中...",
+      "stopAutoRefresh": "停止自动刷新",
+      "startAutoRefresh": "开启自动刷新",
+      "view": "查看"
+    },
+    "error": {
+      "loadFailed": "加载失败"
+    },
+    "details": {
+      "title": "请求详情",
+      "statusTitle": "请求详情 - 状态码 {status}",
+      "inProgress": "请求中",
+      "unknown": "未知",
+      "success": "请求成功完成",
+      "error": "请求失败,以下是详细的错误信息和供应商决策链",
+      "processing": "请求正在进行中,尚未完成",
+      "blocked": {
+        "title": "拦截信息",
+        "type": "拦截类型",
+        "sensitiveWord": "敏感词拦截",
+        "word": "敏感词",
+        "matchType": "匹配类型",
+        "matchTypeContains": "包含匹配",
+        "matchTypeExact": "精确匹配",
+        "matchTypeRegex": "正则表达式",
+        "matchedText": "匹配内容"
+      },
+      "sessionId": "会话 ID",
+      "messagesCount": "消息数量",
+      "messagesLabel": "Messages",
+      "messagesUnit": "条",
+      "clientInfo": "客户端信息",
+      "modelRedirect": {
+        "title": "模型重定向",
+        "requestModel": "请求模型",
+        "actualModel": "实际调用",
+        "billing": "计费说明",
+        "billingDescription": "系统优先使用请求模型({original})的价格计费。如果价格表中不存在该模型,则使用实际调用模型({current})的价格。"
+      },
+      "errorMessage": "错误信息",
+      "providerChain": {
+        "title": "供应商决策链时间线",
+        "totalDuration": "总耗时: {duration}ms"
+      },
+      "noError": {
+        "processing": "请求正在处理中,等待响应...",
+        "success": "请求成功,无错误信息",
+        "default": "暂无详细错误信息"
+      },
+      "clickStatusCode": "点击状态码查看完整时间线"
+    },
+    "providerChain": {
+      "decisionChain": "供应商决策链"
+    },
+    "modelRedirect": {
+      "redirected": "已重定向",
+      "targetModel": "目标模型"
     }
   },
   "leaderboard": {
@@ -74,7 +166,11 @@
     "description": "查看用户和密钥的成本统计排名",
     "tabs": {
       "users": "用户排行",
-      "keys": "密钥排行"
+      "keys": "密钥排行",
+      "userRanking": "用户排行",
+      "providerRanking": "供应商排行",
+      "dailyRanking": "今日排行",
+      "monthlyRanking": "本月排行"
     },
     "columns": {
       "rank": "排名",
@@ -82,24 +178,65 @@
       "totalCost": "总成本",
       "totalRequests": "总请求数",
       "avgCost": "平均成本",
-      "lastActive": "最后活跃时间"
+      "lastActive": "最后活跃时间",
+      "user": "用户",
+      "requests": "请求数",
+      "tokens": "Token 数",
+      "consumedAmount": "消耗金额",
+      "provider": "供应商",
+      "cost": "成本",
+      "successRate": "成功率",
+      "avgResponseTime": "平均响应时间"
+    },
+    "states": {
+      "loading": "加载中...",
+      "noData": "暂无数据",
+      "todayNoData": "今日暂无数据",
+      "monthNoData": "本月暂无数据",
+      "fetchFailed": "获取排行榜数据失败"
+    },
+    "permission": {
+      "title": "需要权限",
+      "restricted": "访问受限",
+      "description": "排行榜功能需要管理员开启\"允许查看全站使用量\"权限。",
+      "adminAction": "开启此权限。",
+      "userAction": "请联系管理员开启此权限。",
+      "systemSettings": "系统设置"
     }
   },
   "sessions": {
     "title": "活跃会话",
     "description": "实时监控当前活跃的 API 会话",
+    "monitoring": "Session 监控",
+    "monitoringDescription": "实时显示活跃和非活跃 Session(每 3 秒自动刷新)",
+    "activeSessions": "活跃 Session(最近 5 分钟)",
+    "inactiveSessions": "非活跃 Session(超过 5 分钟,仅供查看)",
     "columns": {
-      "sessionId": "会话 ID",
+      "sessionId": "Session ID",
       "user": "用户",
+      "key": "密钥",
       "provider": "供应商",
       "model": "模型",
       "startTime": "开始时间",
       "lastActivity": "最后活动",
       "requestCount": "请求数",
-      "status": "状态"
+      "totalInput": "总输入",
+      "totalOutput": "总输出",
+      "totalCost": "总成本",
+      "totalDuration": "总耗时",
+      "status": "状态",
+      "actions": "操作"
+    },
+    "table": {
+      "count": "共 {count} 个{type} Session",
+      "active": "活跃",
+      "inactive": "非活跃",
+      "notCountedInConcurrency": "(不计入并发数)",
+      "refreshing": "刷新中...",
+      "noActiveSessions": "暂无活跃 Session"
     },
     "details": {
-      "title": "会话详情",
+      "title": "Session Messages",
       "messages": "消息列表",
       "info": "会话信息",
       "timeline": "时间线",
@@ -124,16 +261,33 @@
       "providers": "供应商",
       "models": "模型",
       "noDetailedData": "暂无详细数据",
-      "storageTip": "提示:请设置环境变量 STORE_SESSION_MESSAGES=true 以启用 messages 和 response 存储"
-    },
-    "noActiveSessions": "暂无活跃 Session",
-    "back": "返回",
-    "copyMessages": "复制 Messages",
-    "downloadMessages": "下载 Messages",
-    "copied": "已复制",
-    "copyResponse": "复制响应体",
-    "loading": "加载中...",
-    "loadError": "加载失败"
+      "storageTip": "提示:请设置环境变量 STORE_SESSION_MESSAGES=true 以启用 messages 和 response 存储",
+      "clientInfo": "客户端信息",
+      "requestMessages": "请求 Messages",
+      "requestMessagesDescription": "客户端发送的消息内容",
+      "responseBody": "响应体内容",
+      "responseBodyDescription": "服务器返回的完整响应(5分钟 TTL)",
+      "noData": "暂无数据"
+    },
+    "actions": {
+      "back": "返回",
+      "view": "查看",
+      "copyMessages": "复制 Messages",
+      "downloadMessages": "下载 Messages",
+      "copied": "已复制",
+      "copyResponse": "复制响应体"
+    },
+    "status": {
+      "loading": "加载中...",
+      "loadError": "加载失败",
+      "fetchFailed": "获取失败",
+      "unknownError": "未知错误",
+      "storageNotEnabled": "未存储",
+      "storageNotEnabledHint": "提示:请设置环境变量 STORE_SESSION_MESSAGES=true 以启用 messages 存储"
+    },
+    "errors": {
+      "copyFailed": "复制失败"
+    }
   },
   "quotas": {
     "title": "配额管理",
@@ -182,6 +336,47 @@
     "byUsageRate": "按使用率"
   },
   "all": "全部",
+  "nav": {
+    "dashboard": "仪表盘",
+    "usageLogs": "使用记录",
+    "leaderboard": "排行榜",
+    "quotasManagement": "限额管理",
+    "documentation": "文档",
+    "systemSettings": "系统设置",
+    "feedback": "反馈问题",
+    "login": "登录",
+    "logout": "退出登录"
+  },
+  "statistics": {
+    "title": "使用统计",
+    "cost": "消费金额",
+    "calls": "API调用次数",
+    "totalCost": "总消费金额",
+    "totalCalls": "总API调用次数",
+    "timeRange": {
+      "today": "今天",
+      "todayDescription": "今天的使用情况",
+      "7days": "过去 7 天",
+      "7daysDescription": "过去 7 天的使用情况",
+      "30days": "过去 30 天",
+      "30daysDescription": "过去 30 天的使用情况",
+      "default": "使用情况"
+    },
+    "mode": {
+      "keys": "仅显示您名下各密钥的使用统计",
+      "mixed": "展示您的密钥明细和其他用户汇总",
+      "users": "展示所有用户的使用统计"
+    },
+    "legend": {
+      "selectAll": "全选",
+      "deselectAll": "清空",
+      "selected": "已选"
+    },
+    "states": {
+      "noData": "暂无统计数据",
+      "fetchFailed": "获取统计数据失败"
+    }
+  },
   "errors": {
     "fetchSystemSettingsFailed": "获取系统设置失败",
     "fetchFailed": "获取失败",

+ 2 - 0
messages/zh-CN/index.ts

@@ -4,6 +4,7 @@ import customs from "./customs.json";
 import dashboard from "./dashboard.json";
 import errors from "./errors.json";
 import forms from "./forms.json";
+import internal from "./internal.json";
 import notifications from "./notifications.json";
 import quota from "./quota.json";
 import settings from "./settings.json";
@@ -18,6 +19,7 @@ export default {
   dashboard,
   errors,
   forms,
+  internal,
   notifications,
   quota,
   settings,

+ 83 - 0
messages/zh-CN/internal.json

@@ -0,0 +1,83 @@
+{
+  "dataGenerator": {
+    "tabs": {
+      "usage": "用量数据生成",
+      "userBreakdown": "按用户显示用量"
+    },
+    "actions": {
+      "reconfigure": "重新配置参数",
+      "generate": "生成数据",
+      "exportScreenshot": "导出截图",
+      "exportPDF": "导出 PDF"
+    },
+    "params": {
+      "title": "生成参数",
+      "description": "配置生成参数以创建模拟日志数据",
+      "startDate": "起始时间",
+      "endDate": "结束时间",
+      "totalCostCny": "总金额(人民币)",
+      "totalRecords": "总记录数",
+      "models": "包含模型(逗号分隔)",
+      "userIds": "用户ID(逗号分隔)",
+      "providerIds": "供应商ID(逗号分隔)",
+      "serviceName": "服务名称",
+      "required": "*",
+      "placeholders": {
+        "totalCostCny": "如:1000",
+        "totalRecords": "如:500(不填则根据金额计算)",
+        "models": "如:claude-3-5-sonnet,gpt-4(留空则全部)",
+        "userIds": "如:1,2,3(留空则全部)",
+        "providerIds": "如:1,2(留空则全部)",
+        "serviceName": "AI大模型推理服务"
+      }
+    },
+    "summary": {
+      "totalRecords": "总记录数",
+      "totalCost": "总成本",
+      "totalCostCny": "总成本(人民币)",
+      "totalTokens": "总 Token",
+      "timeRange": "时间范围",
+      "to": "至",
+      "uniqueUsers": "总用户数",
+      "totalCalls": "总调用数"
+    },
+    "table": {
+      "usageLogs": {
+        "title": "使用日志",
+        "description": "共 {count} 条记录",
+        "columns": {
+          "time": "时间",
+          "user": "用户",
+          "key": "密钥",
+          "provider": "供应商",
+          "model": "模型",
+          "input": "输入",
+          "output": "输出",
+          "cacheWrite": "缓存写",
+          "cacheRead": "缓存读",
+          "cost": "成本",
+          "duration": "耗时",
+          "status": "状态"
+        }
+      },
+      "userBreakdown": {
+        "title": "用户用量明细",
+        "description": "共 {count} 条记录",
+        "collapseByUser": "按用户折叠",
+        "columns": {
+          "userName": "用户名",
+          "key": "密钥",
+          "serviceModel": "服务模型",
+          "totalCalls": "总调用数",
+          "totalCost": "总调用额度"
+        }
+      }
+    },
+    "status": {
+      "success": "成功"
+    },
+    "errors": {
+      "failed": "Failed to generate logs"
+    }
+  }
+}

+ 155 - 5
messages/zh-CN/quota.json

@@ -8,7 +8,7 @@
     "keysCountSuffix": "个密钥",
     "rpm": "请求限流",
     "todayCost": "今日消费",
-    "exceededNotice": "已超过限额请联系管理员处理"
+    "exceededNotice": "已超过限额,请联系管理员处理"
   },
   "countdown": {
     "reset": "重置倒计时"
@@ -16,19 +16,19 @@
   "windowType": {
     "5h": {
       "label": "5小时滚动",
-      "description": "统计过去5小时内的消费每小时滚动更新"
+      "description": "统计过去5小时内的消费,每小时滚动更新"
     },
     "weekly": {
       "label": "每周重置",
-      "description": "自然周制(周一00:00重置),整周统计"
+      "description": "自然周制(周一00:00重置),整周统计"
     },
     "monthly": {
       "label": "每月重置",
-      "description": "自然月制(每月1日00:00重置),整月统计"
+      "description": "自然月制(每月1日00:00重置),整月统计"
     },
     "daily": {
       "label": "每日重置",
-      "description": "自然日制(每日00:00重置),按日统计"
+      "description": "自然日制(每日00:00重置),按日统计"
     }
   },
   "toolbar": {
@@ -51,5 +51,155 @@
       "30s": "30秒",
       "60s": "60秒"
     }
+  },
+  "layout": {
+    "title": "限额管理",
+    "description": "查看和管理所有层级的限额使用情况",
+    "tabs": {
+      "users": "用户限额",
+      "keys": "密钥限额",
+      "providers": "供应商限额"
+    }
+  },
+  "users": {
+    "title": "用户限额统计",
+    "totalCount": "共 {count} 个用户",
+    "noNote": "无备注",
+    "rpm": {
+      "label": "RPM 限额",
+      "description": "每分钟请求数"
+    },
+    "dailyCost": {
+      "label": "每日消费",
+      "resetAt": "重置于"
+    },
+    "noQuotaData": "无法获取限额信息",
+    "noMatches": "未找到匹配的用户",
+    "noData": "暂无用户数据",
+    "sort": {
+      "name": "按名称",
+      "usage": "按使用率"
+    },
+    "filter": {
+      "all": "全部",
+      "warning": "接近限额 (>60%)",
+      "exceeded": "已超限 (≥100%)"
+    }
+  },
+  "providers": {
+    "title": "供应商限额统计",
+    "totalCount": "共 {count} 个供应商",
+    "filterCount": "显示 {filtered} / {total} 个供应商",
+    "status": {
+      "enabled": "启用",
+      "disabled": "禁用"
+    },
+    "card": {
+      "priority": "优先级",
+      "weight": "权重"
+    },
+    "cost5h": {
+      "label": "5小时消费"
+    },
+    "costWeekly": {
+      "label": "周消费",
+      "resetAt": "重置于"
+    },
+    "costMonthly": {
+      "label": "月消费",
+      "resetAt": "重置于"
+    },
+    "concurrentSessions": {
+      "label": "并发 Session"
+    },
+    "noQuotaSet": "未设置限额",
+    "noQuotaData": "无法获取限额信息",
+    "noMatches": "没有匹配的供应商",
+    "unlimitedSection": "未设置限额的供应商 ({count}个)"
+  },
+  "keys": {
+    "title": "密钥限额统计",
+    "totalCount": "共 {users} 个用户,{keys} 个密钥",
+    "searchPlaceholder": "搜索用户或密钥...",
+    "filterLabel": "筛选条件",
+    "filterCount": "显示 {users} 个用户,{keys} 个密钥",
+    "filter": {
+      "all": "全部密钥",
+      "keyQuota": "仅密钥限额",
+      "userQuotaOnly": "仅用户限额",
+      "warning": "预警(≥60%)",
+      "exceeded": "超限(≥100%)"
+    },
+    "table": {
+      "keyName": "密钥名称",
+      "quotaType": "限额类型",
+      "cost5h": "5小时限额",
+      "costWeekly": "周限额",
+      "costMonthly": "月限额",
+      "concurrentSessions": "并发限制",
+      "status": "状态",
+      "actions": "操作"
+    },
+    "quotaType": {
+      "independent": "独立限额",
+      "inherited": "继承用户"
+    },
+    "status": {
+      "disabled": "已禁用",
+      "restricted": "受限",
+      "normal": "正常"
+    },
+    "noMatches": "没有匹配的用户或密钥",
+    "editDialog": {
+      "title": "设置密钥限额",
+      "description": "密钥: {keyName} ({userName})",
+      "cost5h": {
+        "label": "5小时限额(USD)",
+        "placeholder": "不限制",
+        "current": "当前已用: {currency}{current} / {currency}{limit}"
+      },
+      "costWeekly": {
+        "label": "周限额(USD)",
+        "placeholder": "不限制",
+        "current": "当前已用: {currency}{current} / {currency}{limit}"
+      },
+      "costMonthly": {
+        "label": "月限额(USD)",
+        "placeholder": "不限制",
+        "current": "当前已用: {currency}{current} / {currency}{limit}"
+      },
+      "concurrentSessions": {
+        "label": "并发 Session 限额",
+        "placeholder": "0 = 不限制",
+        "current": "当前并发: {current} / {limit}"
+      },
+      "clearAll": "清除所有限额",
+      "save": "保存",
+      "setQuota": "设置限额",
+      "success": "限额设置成功",
+      "clearSuccess": "限额已清除",
+      "error": "设置失败",
+      "clearError": "清除失败",
+      "retryError": "设置失败,请稍后重试"
+    },
+    "editUserDialog": {
+      "title": "设置用户限额",
+      "description": "用户: {userName}",
+      "rpm": {
+        "label": "每分钟请求数 (RPM)",
+        "placeholder": "60",
+        "current": "当前: {current} / {limit} 请求/分钟"
+      },
+      "dailyQuota": {
+        "label": "每日消费限额(USD)",
+        "placeholder": "100",
+        "current": "今日已用: {currency}{current} / {currency}{limit}"
+      },
+      "save": "保存",
+      "editQuota": "编辑限额",
+      "success": "用户限额设置成功",
+      "error": "设置失败",
+      "retryError": "设置失败,请稍后重试"
+    }
   }
 }

+ 327 - 43
messages/zh-CN/settings.json

@@ -90,12 +90,12 @@
       "apiKeyPlaceholder": "输入 API 密钥",
       "apiKeyOptional": "留空则不更改密钥",
       "weight": "权重",
-      "weightDesc": "加权随机概率。同优先级内,权重越高被选中概率越大。",
+      "weightDesc": "加权随机概率。同优先级内,权重越高被选中概率越大。例如权重 1:2:3 的概率为 16%:33%:50%",
       "priority": "优先级",
-      "priorityDesc": "同优先级内,按成本倍率从低到高排序",
+      "priorityDesc": "数值越小优先级越高(0 最高)。系统只从最高优先级的供应商中选择。建议:主力=0,备用=1,紧急备份=2",
       "enabled": "启用",
       "costMultiplier": "成本倍数",
-      "costMultiplierDesc": "例如: A (成本 1.0x), C (成本 0.8x)",
+      "costMultiplierDesc": "成本计算倍数。官方供应商=1.0,便宜 20%=0.8,贵 20%=1.2(支持最多 4 位小数)",
       "limitConcurrent": "并发 Session 限制",
       "limitConcurrentDesc": "例如: 供应商 C 并发限制 2,当前活跃 Session 数:2",
       "limitAmount5h": "5 小时消费上限 (USD)",
@@ -103,7 +103,7 @@
       "limitAmountWeekly": "周消费上限 (USD)",
       "limitAmountMonthly": "月消费上限 (USD)",
       "modelRedirects": "模型重定向",
-      "modelRedirectsDesc": "将 Claude 模型请求重定向到其他供应商支持的模型",
+      "modelRedirectsDesc": "将 Claude Code 客户端请求的模型(如 claude-sonnet-4.5)重定向到上游供应商实际支持的模型(如 glm-4.6、gemini-pro)。用于成本优化或接入第三方 AI 服务。",
       "sourceModel": "源模型名称",
       "sourceModelPlaceholder": "例如: claude-sonnet-4-5-20250929",
       "sourceModelRequired": "源模型名称不能为空",
@@ -117,7 +117,7 @@
       "proxyUrl": "代理地址",
       "proxyUrlPlaceholder": "例如: http://proxy.example.com:8080 或 socks5://127.0.0.1:1080",
       "proxyFallback": "代理失败降级",
-      "proxyFallbackDesc": "代理连接失败时自动降级到直连",
+      "proxyFallbackDesc": "启用后,代理连接失败时自动尝试直接连接供应商",
       "testProxy": "测试连接",
       "testProxySuccess": "代理连接成功",
       "testProxyFailed": "测试代理连接失败",
@@ -145,7 +145,145 @@
       "searchPlaceholder": "搜索供应商名称、URL、备注...",
       "clearSearch": "清除搜索",
       "limit0Means": "0 表示无限制",
-      "leaveEmpty": "留空表示无限制"
+      "leaveEmpty": "留空表示无限制",
+      "providerName": "服务商名称",
+      "providerNameRequired": "服务商名称 *",
+      "providerNamePlaceholder": "例如: 智谱",
+      "apiAddress": "API 地址",
+      "apiAddressRequired": "API 地址 *",
+      "apiAddressPlaceholder": "例如: https://open.bigmodel.cn/api/anthropic",
+      "apiKeyRequired": "API 密钥 *",
+      "apiKeyLeaveEmpty": "(留空不更改)",
+      "apiKeyLeaveEmptyDesc": "留空则不更改密钥",
+      "apiKeyCurrent": "当前密钥:",
+      "websiteUrl": "供应商官网地址",
+      "websiteUrlPlaceholder": "https://example.com",
+      "websiteUrlDesc": "供应商官网地址,用于快速跳转管理",
+      "websiteUrlInvalid": "请输入有效的供应商官网地址",
+      "expandAll": "展开全部高级配置",
+      "collapseAll": "折叠全部高级配置",
+      "routingConfig": "路由配置",
+      "routingConfigSummary": "{models} 个模型白名单, {redirects} 个重定向",
+      "routingConfigNone": "未配置",
+      "providerTypeDesc": "选择供应商的 API 格式类型。",
+      "providerTypeDisabledNote": "注:Gemini CLI 和 OpenAI Compatible 类型功能正在开发中,暂不可用",
+      "modelRedirectsLabel": "模型重定向配置",
+      "modelRedirectsOptional": "(可选)",
+      "modelRedirectsEmpty": "暂无重定向规则。添加规则后,系统将自动重写请求中的模型名称。",
+      "modelRedirectsCurrentRules": "当前规则 ({count})",
+      "modelRedirectsAddNew": "添加新规则",
+      "modelRedirectsSourceModel": "用户请求的模型",
+      "modelRedirectsSourcePlaceholder": "例如: claude-sonnet-4-5-20250929",
+      "modelRedirectsTargetModel": "实际转发的模型",
+      "modelRedirectsTargetPlaceholder": "例如: glm-4.6",
+      "modelRedirectsSourceRequired": "源模型名称不能为空",
+      "modelRedirectsTargetRequired": "目标模型名称不能为空",
+      "modelRedirectsExists": "模型 \"{model}\" 已存在重定向规则",
+      "joinClaudePool": "加入 Claude 调度池",
+      "joinClaudePoolDesc": "启用后,此供应商将与 Claude 类型供应商一起参与负载均衡调度",
+      "joinClaudePoolHelp": "仅当模型重定向配置中存在映射到 claude-* 模型时可用。启用后,当用户请求 claude-* 模型时,此供应商也会参与调度选择。",
+      "modelWhitelist": "模型白名单",
+      "modelWhitelistDesc": "限制此供应商可以处理的模型。默认情况下,供应商可以处理该类型下的所有模型。",
+      "modelWhitelistLabel": "允许的模型",
+      "modelWhitelistSelected": "已选择 {count} 个模型",
+      "modelWhitelistLoading": "加载中...",
+      "modelWhitelistNotFound": "未找到模型",
+      "modelWhitelistSearchPlaceholder": "搜索模型名称...",
+      "modelWhitelistSelectAll": "全选 ({count})",
+      "modelWhitelistClear": "清空",
+      "modelWhitelistManualAdd": "手动添加模型",
+      "modelWhitelistManualPlaceholder": "输入模型名称(如 gpt-5-turbo)",
+      "modelWhitelistManualDesc": "支持添加任意模型名称(不限于价格表中的模型)",
+      "modelWhitelistAllowAll": "允许所有 {type} 模型",
+      "modelWhitelistAllowAllClause": "允许所有 Claude 模型",
+      "modelWhitelistAllowAllOpenAI": "允许所有 OpenAI 模型",
+      "modelWhitelistSelectedOnly": "仅允许选中的 {count} 个模型。其他模型的请求不会调度到此供应商。",
+      "scheduleParams": "调度参数",
+      "priorityLabel": "优先级",
+      "priorityPlaceholder": "0",
+      "weightLabel": "权重",
+      "weightPlaceholder": "1",
+      "costMultiplierLabel": "成本倍率",
+      "costMultiplierPlaceholder": "1.0",
+      "providerGroupLabel": "供应商分组",
+      "providerGroupPlaceholder": "例如: premium, economy",
+      "providerGroupDesc": "供应商分组标签。只有用户的 providerGroup 与此值匹配时,该用户才能使用此供应商。示例:设置为 \"premium\" 表示只供 providerGroup=\"premium\" 的用户使用",
+      "rateLimitConfig": "限流配置",
+      "rateLimitConfigSummary": "5h: ${fiveHour}, 周: ${weekly}, 月: ${monthly}, 并发: {concurrent}",
+      "rateLimitConfigNone": "无限制",
+      "limit5hLabel": "5小时消费上限 (USD)",
+      "limitWeeklyLabel": "周消费上限 (USD)",
+      "limitMonthlyLabel": "月消费上限 (USD)",
+      "limitConcurrentLabel": "并发 Session 上限",
+      "limitPlaceholderUnlimited": "留空表示无限制",
+      "limitPlaceholder0": "0 表示无限制",
+      "circuitBreakerConfig": "熔断器配置",
+      "circuitBreakerConfigSummary": "{failureThreshold} 次失败 / {openDuration} 分钟熔断 / {successThreshold} 次成功恢复",
+      "circuitBreakerDesc": "供应商连续失败时自动熔断,避免影响整体服务质量",
+      "failureThreshold": "失败阈值(次)",
+      "failureThresholdPlaceholder": "5",
+      "failureThresholdDesc": "连续失败多少次后触发熔断",
+      "openDuration": "熔断时长(分钟)",
+      "openDurationPlaceholder": "30",
+      "openDurationDesc": "熔断后多久自动进入半开状态",
+      "successThreshold": "恢复阈值(次)",
+      "successThresholdPlaceholder": "2",
+      "successThresholdDesc": "半开状态下成功多少次后完全恢复",
+      "proxyConfig": "代理配置",
+      "proxyConfigSummary": "已配置代理",
+      "proxyConfigSummaryFallback": " (启用降级)",
+      "proxyConfigNone": "未配置",
+      "proxyConfigDesc": "配置代理服务器以改善供应商连接性(支持 HTTP、HTTPS、SOCKS4、SOCKS5)",
+      "proxyAddressLabel": "代理地址",
+      "proxyAddressOptional": "(可选)",
+      "proxyAddressPlaceholder": "例如: http://proxy.example.com:8080 或 socks5://127.0.0.1:1080",
+      "proxyAddressFormats": "支持格式:",
+      "proxyFallbackLabel": "代理失败时降级到直连",
+      "proxyTestLabel": "连接测试",
+      "proxyTestButton": "测试连接",
+      "proxyTestTesting": "测试中...",
+      "proxyTestSuccess": "连接成功",
+      "proxyTestFailed": "连接失败",
+      "proxyTestDesc": "测试通过配置的代理访问供应商 URL(使用 HEAD 请求,不消耗额度)",
+      "proxyTestFillUrl": "请先填写供应商 URL",
+      "proxyTestResultSuccess": "连接成功 {via}",
+      "proxyTestViaProxy": "(通过代理)",
+      "proxyTestViaDirect": "(直连)",
+      "proxyTestResponseTime": "响应时间: {time}",
+      "proxyTestStatusCode": "| 状态码: {code}",
+      "proxyTestResultFailed": "连接失败",
+      "proxyTestTimeout": "连接超时(5秒)。请检查:\n1. 代理服务器是否可访问\n2. 代理地址和端口是否正确\n3. 代理认证信息是否正确",
+      "proxyTestProxyError": "代理错误: {error}",
+      "proxyTestNetworkError": "网络错误: {error}",
+      "proxyTestResultMessage": "{message}",
+      "proxyTestResultStatusCode": "状态码: {code}",
+      "proxyTestResultResponseTime": "响应时间: {time}ms",
+      "proxyTestResultConnectionMethod": "连接方式: {via}",
+      "proxyTestResultConnectionMethodProxy": "代理",
+      "proxyTestResultConnectionMethodDirect": "直连",
+      "proxyTestResultErrorType": "错误类型: {type}",
+      "codexStrategyConfig": "Codex Instructions 策略",
+      "codexStrategyConfigAuto": "自动 (推荐)",
+      "codexStrategyConfigForce": "强制官方",
+      "codexStrategyConfigKeep": "透传原样",
+      "codexStrategyDesc": "控制如何处理 Codex 请求的 instructions 字段,影响与上游中转站的兼容性",
+      "codexStrategySelect": "策略选择",
+      "codexStrategyAutoLabel": "自动 (推荐)",
+      "codexStrategyAutoDesc": "透传客户端 instructions,400 错误时自动重试官方 prompt",
+      "codexStrategyForceLabel": "强制官方",
+      "codexStrategyForceDesc": "始终使用官方 Codex CLI instructions(约 4000+ 字)",
+      "codexStrategyKeepLabel": "透传原样",
+      "codexStrategyKeepDesc": "始终透传客户端 instructions,不自动重试(适用于宽松中转站)",
+      "codexStrategyHint": "提示: 部分严格的 Codex 中转站(如 88code、foxcode)需要官方 instructions,选择\"自动\"或\"强制官方\"策略",
+      "confirmAdd": "确认添加",
+      "confirmUpdate": "确认更新",
+      "confirmAddPending": "添加中...",
+      "confirmUpdatePending": "更新中...",
+      "deleteButton": "删除",
+      "validUrlRequired": "请输入有效的 API 地址",
+      "filterProvider": "筛选供应商类型",
+      "filterAllProviders": "全部供应商",
+      "searchClear": "清除搜索"
     },
     "scheduling": "调度策略详解",
     "schedulingDesc": "了解供应商选择如何进行优先级分层、会话复用、负载均衡和故障转移",
@@ -193,8 +331,114 @@
       "circuitBreakerRecovery": "A 在 60 秒后自动恢复到半开状态",
       "circuitBreakerRecovery5h": "5 小时窗口滑动后自动恢复",
       "reset": "手动解除熔断",
-      "resetSuccess": "熔断器已重置"
-    }
+      "resetSuccess": "熔断器已重置",
+      "title": "核心原则",
+      "priorityFirst": "1️⃣ 优先级优先:只从最高优先级(数值最小)的供应商中选择",
+      "costOptimize": "2️⃣ 成本优化:同优先级内,成本倍率低的供应商有更高概率",
+      "healthFilter": "3️⃣ 健康过滤:自动跳过熔断或超限的供应商",
+      "sessionReuse": "4️⃣ 会话复用:连续对话复用同一供应商,节省上下文成本",
+      "scenariosTitle": "交互式场景演示",
+      "bestPracticesTitle": "最佳实践建议",
+      "bestPracticesPriority": "• 优先级设置:核心供应商设为 0,备用供应商设为 1-3",
+      "bestPracticesWeight": "• 权重配置:根据供应商容量设置权重(容量大 = 权重高)",
+      "bestPracticesCost": "• 成本倍率:官方倍率为 1.0,自建服务可设置为 0.8-1.2",
+      "bestPracticesLimit": "• 限额设置:根据预算设置 5 小时、7 天、30 天限额",
+      "bestPracticesConcurrent": "• 并发控制:根据供应商 API 限制设置 Session 并发数",
+      "scenario1Title": "优先级分层选择",
+      "scenario1Desc": "系统首先按优先级过滤,只从最高优先级的供应商中选择",
+      "scenario1Step1": "初始状态",
+      "scenario1Step1Desc": "有 4 个已启用的供应商,优先级各不相同",
+      "scenario1Step1Before": "供应商 A (优先级 0), B (优先级 1), C (优先级 0), D (优先级 2)",
+      "scenario1Step1After": "筛选出最高优先级(0)的供应商:A, C",
+      "scenario1Step1Decision": "只从 A 和 C 中选择,B 和 D 被过滤",
+      "scenario1Step2": "成本排序",
+      "scenario1Step2Desc": "在同优先级内,按成本倍率从低到高排序",
+      "scenario1Step2Before": "A (成本 1.0x), C (成本 0.8x)",
+      "scenario1Step2After": "排序后:C (0.8x), A (1.0x)",
+      "scenario1Step2Decision": "成本更低的 C 有更高的被选中概率",
+      "scenario1Step3": "加权随机",
+      "scenario1Step3Desc": "使用权重进行随机选择,权重越高被选中概率越大",
+      "scenario1Step3Before": "C (权重 3), A (权重 1)",
+      "scenario1Step3After": "C 被选中概率 75%, A 被选中概率 25%",
+      "scenario1Step3Decision": "最终随机选择了 C",
+      "scenario2Title": "用户分组过滤",
+      "scenario2Desc": "如果用户指定了供应商组,系统会优先从该组中选择",
+      "scenario2Step1": "检查用户分组",
+      "scenario2Step1Desc": "用户配置了 providerGroup = 'premium'",
+      "scenario2Step1Before": "所有供应商:A (default), B (premium), C (premium), D (economy)",
+      "scenario2Step1After": "过滤出 'premium' 组:B, C",
+      "scenario2Step1Decision": "只从 B 和 C 中选择",
+      "scenario2Step2": "分组降级",
+      "scenario2Step2Desc": "如果用户组内没有可用供应商,降级到所有供应商",
+      "scenario2Step2Before": "用户组 'vip' 内的供应商全部禁用或超限",
+      "scenario2Step2After": "降级到所有启用的供应商:A, B, C, D",
+      "scenario2Step2Decision": "记录警告并从全局供应商池中选择",
+      "scenario3Title": "健康度过滤(熔断器 + 限流)",
+      "scenario3Desc": "系统自动过滤掉熔断或超限的供应商",
+      "scenario3Step1": "熔断器检查",
+      "scenario3Step1Desc": "连续失败 5 次后熔断器打开,60 秒内不可用",
+      "scenario3Step1Before": "供应商 A 连续失败 5 次,熔断器状态:open",
+      "scenario3Step1After": "A 被过滤,剩余:B, C, D",
+      "scenario3Step1Decision": "A 在 60 秒后自动恢复到半开状态",
+      "scenario3Step2": "金额限流",
+      "scenario3Step2Desc": "检查 5 小时、7 天、30 天的消费额度是否超限",
+      "scenario3Step2Before": "供应商 B 的 5 小时限额 $10,已消耗 $9.8",
+      "scenario3Step2After": "B 被过滤(接近限额),剩余:C, D",
+      "scenario3Step2Decision": "5 小时窗口滑动后自动恢复",
+      "scenario3Step3": "并发 Session 限制",
+      "scenario3Step3Desc": "检查当前活跃 Session 数是否超过配置的并发限制",
+      "scenario3Step3Before": "供应商 C 并发限制 2,当前活跃 Session 数:2",
+      "scenario3Step3After": "C 被过滤(已满),剩余:D",
+      "scenario3Step3Decision": "Session 过期(5 分钟)后自动释放",
+      "scenario4Title": "会话复用机制",
+      "scenario4Desc": "连续对话优先使用同一供应商,利用 Claude 的上下文缓存",
+      "scenario4Step1": "检查历史请求",
+      "scenario4Step1Desc": "查询该 API Key 最近 10 秒内使用的供应商",
+      "scenario4Step1Before": "最近一次请求使用了供应商 B",
+      "scenario4Step1After": "检查 B 是否启用且健康",
+      "scenario4Step1Decision": "B 可用,直接复用,跳过随机选择",
+      "scenario4Step2": "复用失效",
+      "scenario4Step2Desc": "如果上次使用的供应商不可用,则重新选择",
+      "scenario4Step2Before": "上次使用的供应商 B 已被禁用或熔断",
+      "scenario4Step2After": "进入正常选择流程",
+      "scenario4Step2Decision": "从其他可用供应商中选择",
+      "step": "步骤",
+      "before": "过滤前:",
+      "after": "过滤后:",
+      "decision": "决策:"
+    },
+    "section": {
+      "title": "服务商管理",
+      "description": "配置上游服务商的金额限流和并发限制,留空表示无限制。"
+    },
+    "addProvider": "新增服务商",
+    "editProvider": "编辑服务商",
+    "createProvider": "新增服务商",
+    "updateFailed": "更新服务商失败",
+    "deleteSuccess": "删除成功",
+    "confirmDeleteDesc": "确定要删除供应商 \"{name}\" 吗?此操作无法撤销。",
+    "confirmDeleteProvider": "确认删除供应商?",
+    "confirmDeleteProviderDesc": "确定要删除服务商\"{name}\"吗?此操作不可恢复。",
+    "noProviders": "暂无服务商配置",
+    "noProvidersDesc": "添加你的第一个 API 服务商",
+    "toggleSuccess": "供应商已{status}",
+    "toggleSuccessDesc": "供应商 \"{name}\" 状态已更新",
+    "toggleFailed": "状态切换失败",
+    "enabledStatus": "启用",
+    "disabledStatus": "禁用",
+    "searchResults": "找到 {count} 个匹配的供应商",
+    "searchNoResults": "未找到匹配的供应商",
+    "displayCount": "显示 {filtered} / {total} 个供应商",
+    "todayUsage": "今日用量",
+    "todayUsageCount": "{count} 次",
+    "circuitBroken": "熔断中",
+    "official": "官网",
+    "viewKey": "查看完整 API Key",
+    "viewKeyDesc": "请妥善保管,不要泄露给他人",
+    "keyLoading": "加载中...",
+    "resetCircuit": "熔断器已重置",
+    "resetCircuitDesc": "供应商 \"{name}\" 的熔断状态已解除",
+    "resetCircuitFailed": "重置熔断器失败"
   },
   "prices": {
     "title": "价格表",
@@ -235,7 +479,7 @@
   "sensitiveWords": {
     "title": "敏感词管理",
     "description": "配置敏感词过滤规则,拦截包含敏感内容的请求。",
-    "list": {
+    "section": {
       "title": "敏感词列表",
       "description": "被敏感词拦截的请求不会转发到上游,也不会计费。支持包含匹配、精确匹配和正则表达式三种模式。"
     },
@@ -253,29 +497,38 @@
     "toggleFailed": "状态切换失败",
     "toggleFailedError": "状态切换失败:",
     "refreshCache": "刷新缓存",
-    "refreshing": "刷新中...",
-    "refreshSuccess": "刷新完成",
-    "refreshFailed": "刷新缓存失败",
-    "form": {
-      "word": "敏感词",
-      "wordPlaceholder": "输入敏感词或正则表达式",
-      "matchMode": "匹配模式",
-      "modeContains": "包含匹配",
-      "modeExact": "精确匹配",
-      "modeRegex": "正则表达式",
-      "enabled": "启用",
-      "remark": "备注",
-      "remarkPlaceholder": "可选:添加说明...",
-      "confirm": "确认添加",
-      "confirmUpdate": "确认更新"
+    "refreshCacheSuccess": "缓存刷新成功,已加载 {count} 个敏感词",
+    "refreshCacheFailed": "刷新缓存失败",
+    "cacheStats": "缓存统计: 包含({containsCount}) 精确({exactCount}) 正则({regexCount})",
+    "emptyState": "暂无敏感词,点击右上角\"添加敏感词\"开始配置。",
+    "confirmDelete": "确定要删除敏感词\"{word}\"吗?",
+    "dialog": {
+      "addTitle": "添加敏感词",
+      "addDescription": "配置敏感词过滤规则,被命中的请求将不会转发到上游。",
+      "editTitle": "编辑敏感词",
+      "editDescription": "修改敏感词配置,更改后将自动刷新缓存。",
+      "wordLabel": "敏感词 *",
+      "wordPlaceholder": "输入敏感词...",
+      "wordRequired": "请输入敏感词",
+      "matchTypeLabel": "匹配类型 *",
+      "matchTypeContains": "包含匹配 - 文本中包含该词即拦截",
+      "matchTypeExact": "精确匹配 - 完全匹配该词才拦截",
+      "matchTypeRegex": "正则表达式 - 支持复杂模式匹配",
+      "descriptionLabel": "说明",
+      "descriptionPlaceholder": "可选:添加说明...",
+      "creating": "创建中...",
+      "saving": "保存中..."
     },
-    "cache": {
-      "title": "缓存统计",
-      "hitRate": "命中率",
-      "totalHits": "总命中数",
-      "totalChecks": "总检查数",
-      "cacheSize": "缓存大小",
-      "lastUpdated": "最后更新"
+    "table": {
+      "word": "敏感词",
+      "matchType": "匹配类型",
+      "matchTypeContains": "包含匹配",
+      "matchTypeExact": "精确匹配",
+      "matchTypeRegex": "正则表达式",
+      "description": "说明",
+      "status": "状态",
+      "createdAt": "创建时间",
+      "actions": "操作"
     }
   },
   "logs": {
@@ -343,21 +596,52 @@
   "clientVersions": {
     "title": "客户端升级提醒",
     "description": "管理客户端版本要求,确保用户使用最新的稳定版本。VSCode 插件和 CLI 作为独立客户端分别管理版本。",
+    "section": {
+      "settings": {
+        "title": "升级提醒设置",
+        "description": "启用后,系统将自动检测客户端版本并拦截旧版本用户的请求。"
+      },
+      "distribution": {
+        "title": "客户端版本分布",
+        "description": "显示过去 7 天内活跃用户的客户端版本信息。每种客户端类型独立统计 GA 版本。"
+      }
+    },
+    "empty": {
+      "title": "暂无客户端数据",
+      "description": "过去 7 天内没有活跃用户使用可识别的客户端"
+    },
     "toggle": {
-      "title": "升级提醒设置",
-      "description": "启用后,系统将自动检测客户端版本并拦截旧版本用户的请求。",
-      "note": "注意: VSCode 插件(claude-vscode)和纯CLI(claude-cli)采用独立的版本检测策略,互不影响。",
-      "enable": "启用客户端版本检查",
-      "disable": "禁用客户端版本检查",
+      "enable": "启用升级提醒",
+      "description": "启用后,系统将拦截使用旧版本客户端的请求",
       "enableSuccess": "已启用客户端版本检查",
       "disableSuccess": "已关闭客户端版本检查",
-      "toggleFailed": "状态切换失败"
+      "toggleFailed": "更新失败"
     },
-    "stats": {
-      "title": "客户端版本分布",
-      "description": "显示过去 7 天内活跃用户的客户端版本信息。每种客户端类型独立统计 GA 版本。",
-      "noData": "暂无客户端数据",
-      "noDataDesc": "过去 7 天内没有活跃用户使用可识别的客户端"
+    "features": {
+      "title": "功能说明",
+      "whatHappens": "启用后会发生什么:",
+      "autoDetect": "系统会自动检测每种客户端的最新稳定版本(GA 版本)",
+      "gaRule": "判定规则:",
+      "gaRuleDesc": "当某个版本被 1 个以上用户使用时,视为 GA 版本",
+      "activeWindow": "活跃窗口:",
+      "activeWindowDesc": "仅统计过去 7 天内有请求的用户",
+      "blockOldVersion": "使用旧版本的用户将收到 HTTP 400 错误,无法继续使用服务",
+      "errorMessage": "错误提示中包含当前版本和需要升级的版本号",
+      "recommendation": "推荐做法:",
+      "recommendationDesc": "先观察下方的版本分布,确认新版本稳定后再启用。"
+    },
+    "table": {
+      "internalType": "内部类型:",
+      "currentGA": "当前 GA 版本:",
+      "usersCount": "{count} 位用户",
+      "user": "用户",
+      "version": "当前版本",
+      "lastActive": "最后活跃时间",
+      "status": "状态",
+      "noUsers": "暂无用户数据",
+      "latest": "最新",
+      "needsUpgrade": "需升级",
+      "unknown": "未知"
     }
   },
   "notifications": {
@@ -433,4 +717,4 @@
     "loadFailed": "加载通知设置失败",
     "unknownError": "操作过程中出现异常"
   }
-}
+}

+ 19 - 1
messages/zh-CN/usage.json

@@ -387,6 +387,23 @@
           "开始使用!"
         ]
       }
+    },
+
+    "startup": {
+      "title": "启动 droid",
+      "description": "在项目目录下运行:",
+      "initNote": "首次启动时,droid 会进行初始化配置。"
+    },
+
+    "commonIssues": {
+      "title": "常见问题",
+      "commandNotFound": "1. 命令未找到",
+      "commandNotFoundWindows": [
+        "确保 npm 全局路径(通常是 C:\\Users\\你的用户名\\AppData\\Roaming\\npm)已添加到系统 PATH",
+        "重新打开 PowerShell 窗口"
+      ],
+      "commandNotFoundUnix": "检查 npm 全局安装路径并添加到 PATH(如果不在)",
+      "updateCli": "2. 更新 droid"
     }
   },
 
@@ -444,6 +461,7 @@
 
   "ui": {
     "mainContent": "文档内容",
-    "main": "main"
+    "main": "main",
+    "currentSiteAddress": "当前站点地址"
   }
 }

+ 2 - 0
messages/zh-TW/index.ts

@@ -10,6 +10,7 @@ import settings from "./settings.json";
 import ui from "./ui.json";
 import usage from "./usage.json";
 import validation from "./validation.json";
+import internal from "./internal.json";
 
 export default {
   auth,
@@ -24,4 +25,5 @@ export default {
   ui,
   usage,
   validation,
+  internal,
 };

+ 83 - 0
messages/zh-TW/internal.json

@@ -0,0 +1,83 @@
+{
+  "dataGenerator": {
+    "tabs": {
+      "usage": "用量數據生成",
+      "userBreakdown": "按用戶顯示用量"
+    },
+    "actions": {
+      "reconfigure": "重新配置參數",
+      "generate": "生成數據",
+      "exportScreenshot": "導出截圖",
+      "exportPDF": "導出 PDF"
+    },
+    "params": {
+      "title": "生成參數",
+      "description": "配置生成參數以創建模擬日誌數據",
+      "startDate": "起始時間",
+      "endDate": "結束時間",
+      "totalCostCny": "總金額(人民幣)",
+      "totalRecords": "總記錄數",
+      "models": "包含模型(逗號分隔)",
+      "userIds": "用戶ID(逗號分隔)",
+      "providerIds": "供應商ID(逗號分隔)",
+      "serviceName": "服務名稱",
+      "required": "*",
+      "placeholders": {
+        "totalCostCny": "如:1000",
+        "totalRecords": "如:500(不填則根據金額計算)",
+        "models": "如:claude-3-5-sonnet,gpt-4(留空則全部)",
+        "userIds": "如:1,2,3(留空則全部)",
+        "providerIds": "如:1,2(留空則全部)",
+        "serviceName": "AI大模型推理服務"
+      }
+    },
+    "summary": {
+      "totalRecords": "總記錄數",
+      "totalCost": "總成本",
+      "totalCostCny": "總成本(人民幣)",
+      "totalTokens": "總 Token",
+      "timeRange": "時間範圍",
+      "to": "至",
+      "uniqueUsers": "總用戶數",
+      "totalCalls": "總調用數"
+    },
+    "table": {
+      "usageLogs": {
+        "title": "使用日誌",
+        "description": "共 {count} 條記錄",
+        "columns": {
+          "time": "時間",
+          "user": "用戶",
+          "key": "密鑰",
+          "provider": "供應商",
+          "model": "模型",
+          "input": "輸入",
+          "output": "輸出",
+          "cacheWrite": "緩存寫",
+          "cacheRead": "緩存讀",
+          "cost": "成本",
+          "duration": "耗時",
+          "status": "狀態"
+        }
+      },
+      "userBreakdown": {
+        "title": "用戶用量明細",
+        "description": "共 {count} 條記錄",
+        "collapseByUser": "按用戶折疊",
+        "columns": {
+          "userName": "用戶名",
+          "key": "密鑰",
+          "serviceModel": "服務模型",
+          "totalCalls": "總調用數",
+          "totalCost": "總調用額度"
+        }
+      }
+    },
+    "status": {
+      "success": "成功"
+    },
+    "errors": {
+      "failed": "生成日誌失敗"
+    }
+  }
+}

+ 171 - 21
messages/zh-TW/quota.json

@@ -1,49 +1,49 @@
 {
   "header": {
-    "title": "使用者配額",
+    "title": "用户配额",
     "role": {
-      "admin": "管理",
-      "user": "使用者"
+      "admin": "管理",
+      "user": "用户"
     },
-    "keysCountSuffix": "個密鑰",
-    "rpm": "求限流",
-    "todayCost": "今日消",
-    "exceededNotice": "已超過限額,請聯絡管理員處理"
+    "keysCountSuffix": "个密钥",
+    "rpm": "求限流",
+    "todayCost": "今日消",
+    "exceededNotice": "已超过限额,请联系管理员处理"
   },
   "countdown": {
-    "reset": "重置倒計時"
+    "reset": "重置倒计时"
   },
   "windowType": {
     "5h": {
-      "label": "5小時滾動",
-      "description": "統計過去5小時內的消費,每小時滾動更新"
+      "label": "5小时滚动",
+      "description": "统计过去5小时内的消费,每小时滚动更新"
     },
     "weekly": {
-      "label": "每重置",
-      "description": "自然週制(週一00:00重置),整週統計"
+      "label": "每重置",
+      "description": "自然周制(周一00:00重置),整周统计"
     },
     "monthly": {
       "label": "每月重置",
-      "description": "自然月制(每月1日00:00重置),整月統計"
+      "description": "自然月制(每月1日00:00重置),整月统计"
     },
     "daily": {
       "label": "每日重置",
-      "description": "自然日制(每日00:00重置),按日統計"
+      "description": "自然日制(每日00:00重置),按日统计"
     }
   },
   "toolbar": {
-    "searchPlaceholder": "搜尋使用者名稱或密鑰",
-    "filter": "篩選",
+    "searchPlaceholder": "搜索用户名或密钥",
+    "filter": "筛选",
     "sort": "排序",
-    "refresh": "重新整理",
-    "autoRefresh": "自動重新整理",
+    "refresh": "刷新",
+    "autoRefresh": "自动刷新",
     "filterOptions": {
       "all": "全部",
-      "warning": "警",
+      "warning": "警",
       "exceeded": "已超限"
     },
     "sortOptions": {
-      "name": "按名",
+      "name": "按名",
       "usage": "按使用率"
     },
     "interval": {
@@ -51,5 +51,155 @@
       "30s": "30秒",
       "60s": "60秒"
     }
+  },
+  "layout": {
+    "title": "限額管理",
+    "description": "查看和管理所有層級的限額使用情況",
+    "tabs": {
+      "users": "用戶限額",
+      "keys": "密鑰限額",
+      "providers": "供應商限額"
+    }
+  },
+  "users": {
+    "title": "用戶限額統計",
+    "totalCount": "共 {count} 個用戶",
+    "noNote": "無備註",
+    "rpm": {
+      "label": "RPM 限額",
+      "description": "每分鐘請求數"
+    },
+    "dailyCost": {
+      "label": "每日消費",
+      "resetAt": "重置於"
+    },
+    "noQuotaData": "無法獲取限額資訊",
+    "noMatches": "未找到匹配的用戶",
+    "noData": "暫無用戶數據",
+    "sort": {
+      "name": "按名稱",
+      "usage": "按使用率"
+    },
+    "filter": {
+      "all": "全部",
+      "warning": "接近限額 (>60%)",
+      "exceeded": "已超限 (≥100%)"
+    }
+  },
+  "providers": {
+    "title": "供應商限額統計",
+    "totalCount": "共 {count} 個供應商",
+    "filterCount": "顯示 {filtered} / {total} 個供應商",
+    "status": {
+      "enabled": "啟用",
+      "disabled": "禁用"
+    },
+    "card": {
+      "priority": "優先級",
+      "weight": "權重"
+    },
+    "cost5h": {
+      "label": "5小時消費"
+    },
+    "costWeekly": {
+      "label": "周消費",
+      "resetAt": "重置於"
+    },
+    "costMonthly": {
+      "label": "月消費",
+      "resetAt": "重置於"
+    },
+    "concurrentSessions": {
+      "label": "並發 Session"
+    },
+    "noQuotaSet": "未設置限額",
+    "noQuotaData": "無法獲取限額資訊",
+    "noMatches": "沒有匹配的供應商",
+    "unlimitedSection": "未設置限額的供應商 ({count}個)"
+  },
+  "keys": {
+    "title": "密鑰限額統計",
+    "totalCount": "共 {users} 個用戶,{keys} 個密鑰",
+    "searchPlaceholder": "搜索用戶或密鑰...",
+    "filterLabel": "篩選條件",
+    "filterCount": "顯示 {users} 個用戶,{keys} 個密鑰",
+    "filter": {
+      "all": "全部密鑰",
+      "keyQuota": "僅密鑰限額",
+      "userQuotaOnly": "僅用戶限額",
+      "warning": "預警(≥60%)",
+      "exceeded": "超限(≥100%)"
+    },
+    "table": {
+      "keyName": "密鑰名稱",
+      "quotaType": "限額類型",
+      "cost5h": "5小時限額",
+      "costWeekly": "周限額",
+      "costMonthly": "月限額",
+      "concurrentSessions": "並發限制",
+      "status": "狀態",
+      "actions": "操作"
+    },
+    "quotaType": {
+      "independent": "獨立限額",
+      "inherited": "繼承用戶"
+    },
+    "status": {
+      "disabled": "已禁用",
+      "restricted": "受限",
+      "normal": "正常"
+    },
+    "noMatches": "沒有匹配的用戶或密鑰",
+    "editDialog": {
+      "title": "設置密鑰限額",
+      "description": "密钥: {keyName} ({userName})",
+      "cost5h": {
+        "label": "5小时限额(USD)",
+        "placeholder": "不限制",
+        "current": "当前已用: {currency}{current} / {currency}{limit}"
+      },
+      "costWeekly": {
+        "label": "周限额(USD)",
+        "placeholder": "不限制",
+        "current": "当前已用: {currency}{current} / {currency}{limit}"
+      },
+      "costMonthly": {
+        "label": "月限额(USD)",
+        "placeholder": "不限制",
+        "current": "当前已用: {currency}{current} / {currency}{limit}"
+      },
+      "concurrentSessions": {
+        "label": "并发 Session 限额",
+        "placeholder": "0 = 不限制",
+        "current": "当前并发: {current} / {limit}"
+      },
+      "clearAll": "清除所有限额",
+      "save": "保存",
+      "setQuota": "设置限额",
+      "success": "限額設置成功",
+      "clearSuccess": "限额已清除",
+      "error": "设置失败",
+      "clearError": "清除失败",
+      "retryError": "设置失败,请稍后重试"
+    },
+    "editUserDialog": {
+      "title": "設置用戶限額",
+      "description": "用户: {userName}",
+      "rpm": {
+        "label": "每分钟请求数 (RPM)",
+        "placeholder": "60",
+        "current": "当前: {current} / {limit} 请求/分钟"
+      },
+      "dailyQuota": {
+        "label": "每日消费限额(USD)",
+        "placeholder": "100",
+        "current": "今日已用: {currency}{current} / {currency}{limit}"
+      },
+      "save": "保存",
+      "editQuota": "编辑限额",
+      "success": "用户限额设置成功",
+      "error": "设置失败",
+      "retryError": "设置失败,请稍后重试"
+    }
   }
-}
+}

+ 115 - 9
messages/zh-TW/settings.json

@@ -201,24 +201,66 @@
     "description": "管理平台基礎設定與模型價格",
     "subtitle": "模型價格",
     "subtitleDesc": "管理 AI 模型的價格設定",
-    "search": "搜尋模型名稱...",
-    "sync": "同步 LiteLLM 價格",
+    "search": {
+      "placeholder": "搜尋模型名稱...",
+      "totalModels": "共 {total} 個模型價格",
+      "searchResults": "(搜尋結果:{count} 個)",
+      "lastUpdated": "最後更新:"
+    },
+    "sync": {
+      "button": "同步 LiteLLM 價格",
+      "syncing": "同步中...",
+      "successWithChanges": "同步成功:新增 {added} 個,更新 {updated} 個,未變化 {unchanged} 個",
+      "successNoChanges": "所有 {unchanged} 個模型價格均為最新",
+      "noModels": "未找到支援的模型價格",
+      "failed": "同步失敗",
+      "failedError": "同步失敗,請重試",
+      "failedNoResult": "同步成功但未返回處理結果",
+      "partialFailure": "{failed} 個模型處理失敗"
+    },
     "syncing": "同步中...",
     "syncSuccess": "價格表更新成功",
     "syncFailed": "同步失敗",
     "syncFailedError": "同步失敗:",
     "syncNoResult": "價格表更新成功但未返回處理結果",
-    "upload": "更新模型價格表",
+    "upload": {
+      "button": "更新價格表",
+      "updating": "更新中...",
+      "dialogTitle": "更新模型價格表",
+      "dialogDescription": "選擇包含模型價格資料的 JSON 檔案以更新價格表",
+      "selectFile": "點選選擇JSON檔案或拖曳到此處",
+      "fileSizeLimit": "檔案大小不超過10MB",
+      "selectFileButton": "選擇檔案",
+      "invalidFileType": "請選擇JSON檔案",
+      "fileTooLarge": "檔案大小不能超過10MB",
+      "success": "價格表更新成功",
+      "failed": "更新失敗,請重試",
+      "failedError": "更新失敗:",
+      "noResult": "價格表更新成功但未返回處理結果",
+      "updateInProgress": "正在更新模型價格...",
+      "enterDashboard": "進入控制面板",
+      "completed": "完成"
+    },
     "uploadSuccess": "價格表更新成功",
     "uploadFailed": "取得價格資料失敗:",
     "noData": "系統已內置價格表,請透過上方按鈕同步或更新",
     "noModels": "未找到支援的模型價格",
     "table": {
-      "model": "模型",
-      "inputPrice": "輸入價格",
-      "outputPrice": "輸出價格",
-      "cachePrice": "快取價格",
-      "updatedAt": "更新時間"
+      "modelName": "模型名稱",
+      "type": "類型",
+      "provider": "提供商",
+      "inputPrice": "輸入價格 ($/M)",
+      "outputPrice": "輸出價格 ($/M)",
+      "updatedAt": "更新時間",
+      "typeChat": "對話",
+      "typeImageGen": "影像生成",
+      "typeCompletion": "補全",
+      "typeUnknown": "未知",
+      "imagePrice": "${price}/img",
+      "loading": "載入中...",
+      "noResults": "未找到匹配的模型",
+      "noData": "暫無價格資料",
+      "noDataHint": "系統已內建價格表,請透過上方按鈕同步或更新"
     },
     "dialog": {
       "title": "更新模型價格表",
@@ -230,6 +272,33 @@
       "uploading": "上傳中...",
       "readError": "取得金鑰失敗",
       "getError": "取得金鑰失敗"
+    },
+    "section": {
+      "title": "模型價格",
+      "description": "管理 AI 模型的價格配置"
+    },
+    "pagination": {
+      "perPage": "每頁顯示:",
+      "showing": "顯示第 {start} - {end} 條,共 {total} 條記錄",
+      "previous": "上一頁",
+      "next": "下一頁"
+    },
+    "uploadDialog": {
+      "notes": {
+        "litellm": "• 系統已內建 LiteLLM 價格表,如需更新可使用左側「同步 LiteLLM 價格」按鈕",
+        "manual": "• 也可以手動下載",
+        "link": "最新價格表",
+        "linkAnd": "並透過此按鈕更新",
+        "support": "• 支援 Claude 和 OpenAI 模型(claude-, gpt-, o1-, o3- 前綴)"
+      },
+      "result": {
+        "total": "處理總數",
+        "added": "新增模型 ({count})",
+        "updated": "更新模型 ({count})",
+        "unchanged": "未變化 ({count})",
+        "failed": "處理失敗 ({count})",
+        "showMore": "等{count}個"
+      }
     }
   },
   "sensitiveWords": {
@@ -276,6 +345,43 @@
       "totalChecks": "總檢查數",
       "cacheSize": "快取大小",
       "lastUpdated": "最後更新"
+    },
+    "section": {
+      "title": "敏感詞列表",
+      "description": "被敏感詞攔截的請求不會轉發到上游,也不會計費。支援包含匹配、精確匹配和正則表達式三種模式。"
+    },
+    "refreshCacheSuccess": "快取刷新成功,已載入 {count} 個敏感詞",
+    "refreshCacheFailed": "刷新快取失敗",
+    "cacheStats": "快取統計:包含({containsCount}) 精確({exactCount}) 正則({regexCount})",
+    "emptyState": "暫無敏感詞,點選右上角「新增敏感詞」開始配置。",
+    "confirmDelete": "確定要刪除敏感詞「{word}」嗎?",
+    "dialog": {
+      "addTitle": "新增敏感詞",
+      "addDescription": "配置敏感詞過濾規則,被命中的請求將不會轉發到上游。",
+      "editTitle": "編輯敏感詞",
+      "editDescription": "修改敏感詞配置,更改後將自動刷新快取。",
+      "wordLabel": "敏感詞 *",
+      "wordPlaceholder": "輸入敏感詞...",
+      "wordRequired": "請輸入敏感詞",
+      "matchTypeLabel": "匹配類型 *",
+      "matchTypeContains": "包含匹配 - 文字中包含該詞即攔截",
+      "matchTypeExact": "精確匹配 - 完全匹配該詞才攔截",
+      "matchTypeRegex": "正則表達式 - 支援複雜模式匹配",
+      "descriptionLabel": "說明",
+      "descriptionPlaceholder": "選填:新增說明...",
+      "creating": "建立中...",
+      "saving": "儲存中..."
+    },
+    "table": {
+      "word": "敏感詞",
+      "matchType": "匹配類型",
+      "matchTypeContains": "包含匹配",
+      "matchTypeExact": "精確匹配",
+      "matchTypeRegex": "正則表達式",
+      "description": "說明",
+      "status": "狀態",
+      "createdAt": "建立時間",
+      "actions": "操作"
     }
   },
   "logs": {
@@ -433,4 +539,4 @@
     "loadFailed": "載入通知設定失敗",
     "unknownError": "操作過程中出現異常"
   }
-}
+}

+ 19 - 1
messages/zh-TW/usage.json

@@ -387,6 +387,23 @@
           "開始使用!"
         ]
       }
+    },
+
+    "startup": {
+      "title": "啟動 droid",
+      "description": "在項目目錄下運行以下命令:",
+      "initNote": "首次啟動時,droid 會進行初始化配置。"
+    },
+
+    "commonIssues": {
+      "title": "常見問題",
+      "commandNotFound": "1. 找不到命令",
+      "commandNotFoundWindows": [
+        "確保 npm 全局路徑(通常為 C:\\Users\\your-username\\AppData\\Roaming\\npm)已添加到系統 PATH",
+        "重新打開 PowerShell 窗口"
+      ],
+      "commandNotFoundUnix": "檢查 npm 全局安裝路徑,並添加到 PATH(如果尚未添加)",
+      "updateCli": "2. 更新 droid"
     }
   },
 
@@ -444,6 +461,7 @@
 
   "ui": {
     "mainContent": "文檔內容",
-    "main": "main"
+    "main": "main",
+    "currentSiteAddress": "當前站點地址"
   }
 }

+ 14 - 11
src/app/[locale]/dashboard/_components/dashboard-header.tsx

@@ -6,23 +6,26 @@ import { DashboardNav, type DashboardNavItem } from "./dashboard-nav";
 import { UserMenu } from "./user-menu";
 import { VersionUpdateNotifier } from "@/components/customs/version-update-notifier";
 import { LanguageSwitcher } from "@/components/ui/language-switcher";
+import { useTranslations } from "next-intl";
 
 interface DashboardHeaderProps {
   session: AuthSession | null;
 }
 
-const NAV_ITEMS: (DashboardNavItem & { adminOnly?: boolean })[] = [
-  { href: "/dashboard", label: "仪表盘" },
-  { href: "/dashboard/logs", label: "使用记录" },
-  { href: "/dashboard/leaderboard", label: "排行榜" },
-  { href: "/dashboard/quotas", label: "限额管理" },
-  { href: "/usage-doc", label: "文档" },
-  { href: "/settings", label: "系统设置", adminOnly: true },
-  { href: "https://github.com/ding113/claude-code-hub/issues", label: "反馈问题", external: true },
-];
-
 export function DashboardHeader({ session }: DashboardHeaderProps) {
+  const t = useTranslations("dashboard.nav");
   const isAdmin = session?.user.role === "admin";
+
+  const NAV_ITEMS: (DashboardNavItem & { adminOnly?: boolean })[] = [
+    { href: "/dashboard", label: t("dashboard") },
+    { href: "/dashboard/logs", label: t("usageLogs") },
+    { href: "/dashboard/leaderboard", label: t("leaderboard") },
+    { href: "/dashboard/quotas", label: t("quotasManagement") },
+    { href: "/usage-doc", label: t("documentation") },
+    { href: "/settings", label: t("systemSettings"), adminOnly: true },
+    { href: "https://github.com/ding113/claude-code-hub/issues", label: t("feedback"), external: true },
+  ];
+
   const items = NAV_ITEMS.filter((item) => !item.adminOnly || isAdmin);
 
   return (
@@ -36,7 +39,7 @@ export function DashboardHeader({ session }: DashboardHeaderProps) {
             <UserMenu user={session.user} />
           ) : (
             <Button asChild size="sm" variant="outline">
-              <Link href="/login">登录</Link>
+              <Link href="/login">{t("login")}</Link>
             </Button>
           )}
         </div>

+ 1 - 2
src/app/[locale]/dashboard/_components/dashboard-nav.tsx

@@ -1,7 +1,6 @@
 "use client";
 
-import { Link } from "@/i18n/routing";
-import { usePathname } from "next/navigation";
+import { Link, usePathname } from "@/i18n/routing";
 
 import { cn } from "@/lib/utils";
 

+ 19 - 17
src/app/[locale]/dashboard/_components/statistics/chart.tsx

@@ -10,6 +10,7 @@ import { ChartConfig, ChartContainer, ChartLegend, ChartTooltip } from "@/compon
 
 import type { UserStatisticsData, TimeRange } from "@/types/statistics";
 import { TimeRangeSelector } from "./time-range-selector";
+import { useTranslations } from "next-intl";
 
 // 固定的调色盘,确保新增用户也能获得可辨识的颜色
 const USER_COLOR_PALETTE = [
@@ -57,6 +58,7 @@ export function UserStatisticsChart({
   onTimeRangeChange,
   currencyCode = "USD",
 }: UserStatisticsChartProps) {
+  const t = useTranslations("dashboard.statistics");
   const [activeChart, setActiveChart] = React.useState<"cost" | "calls">("cost");
 
   // 用户选择状态(仅 Admin 用 users 模式时启用)
@@ -102,10 +104,10 @@ export function UserStatisticsChart({
   const chartConfig = React.useMemo(() => {
     const config: ChartConfig = {
       cost: {
-        label: "消费金额",
+        label: t("cost"),
       },
       calls: {
-        label: "API调用次数",
+        label: t("calls"),
       },
     };
 
@@ -263,23 +265,23 @@ export function UserStatisticsChart({
   const getTimeRangeDescription = () => {
     switch (data.timeRange) {
       case "today":
-        return "今天的使用情况";
+        return t("timeRange.todayDescription");
       case "7days":
-        return "过去 7 天的使用情况";
+        return t("timeRange.7daysDescription");
       case "30days":
-        return "过去 30 天的使用情况";
+        return t("timeRange.30daysDescription");
       default:
-        return "使用情况";
+        return t("timeRange.default");
     }
   };
 
   const getAggregationLabel = () => {
     if (data.mode === "keys") {
-      return "仅显示您名下各密钥的使用统计";
+      return t("mode.keys");
     } else if (data.mode === "mixed") {
-      return "展示您的密钥明细和其他用户汇总";
+      return t("mode.mixed");
     } else {
-      return "展示所有用户的使用统计";
+      return t("mode.users");
     }
   };
 
@@ -292,7 +294,7 @@ export function UserStatisticsChart({
         )}
       >
         <div className="flex flex-1 flex-col justify-center gap-1 px-6 pt-4 pb-3 lg:!py-0">
-          <CardTitle>使用统计</CardTitle>
+          <CardTitle>{t("title")}</CardTitle>
           <CardDescription>
             {getTimeRangeDescription()} · {getAggregationLabel()}
           </CardDescription>
@@ -313,7 +315,7 @@ export function UserStatisticsChart({
               className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l lg:border-t-0 lg:border-l lg:px-8 lg:py-6"
               onClick={() => setActiveChart("cost")}
             >
-              <span className="text-muted-foreground text-xs">总消费金额</span>
+              <span className="text-muted-foreground text-xs">{t("totalCost")}</span>
               <span className="text-lg leading-none font-bold sm:text-3xl">
                 {formatCurrency(visibleTotals.cost, currencyCode)}
               </span>
@@ -323,7 +325,7 @@ export function UserStatisticsChart({
               className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l lg:border-t-0 lg:border-l lg:px-8 lg:py-6"
               onClick={() => setActiveChart("calls")}
             >
-              <span className="text-muted-foreground text-xs">总API调用次数</span>
+              <span className="text-muted-foreground text-xs">{t("totalCalls")}</span>
               <span className="text-lg leading-none font-bold sm:text-3xl">
                 {visibleTotals.calls.toLocaleString()}
               </span>
@@ -339,7 +341,7 @@ export function UserStatisticsChart({
             className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 px-6 py-3 text-left even:border-l transition-colors hover:bg-muted/30"
             onClick={() => setActiveChart("cost")}
           >
-            <span className="text-muted-foreground text-xs">总消费金额</span>
+            <span className="text-muted-foreground text-xs">{t("totalCost")}</span>
             <span className="text-lg leading-none font-bold sm:text-xl">
               {formatCurrency(visibleTotals.cost, currencyCode)}
             </span>
@@ -349,7 +351,7 @@ export function UserStatisticsChart({
             className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 px-6 py-3 text-left even:border-l transition-colors hover:bg-muted/30"
             onClick={() => setActiveChart("calls")}
           >
-            <span className="text-muted-foreground text-xs">总API调用次数</span>
+            <span className="text-muted-foreground text-xs">{t("totalCalls")}</span>
             <span className="text-lg leading-none font-bold sm:text-xl">
               {visibleTotals.calls.toLocaleString()}
             </span>
@@ -491,18 +493,18 @@ export function UserStatisticsChart({
                         onClick={selectAllUsers}
                         className="text-xs text-muted-foreground hover:text-foreground px-2 py-1 rounded hover:bg-muted/50 transition-colors"
                       >
-                        全选 ({data.users.length})
+                        {t("legend.selectAll")} ({data.users.length})
                       </button>
                       <span className="text-muted-foreground">·</span>
                       <button
                         onClick={deselectAllUsers}
                         className="text-xs text-muted-foreground hover:text-foreground px-2 py-1 rounded hover:bg-muted/50 transition-colors"
                       >
-                        清空
+                        {t("legend.deselectAll")}
                       </button>
                       <span className="text-muted-foreground">·</span>
                       <span className="text-xs text-muted-foreground">
-                        已选 {selectedUserIds.size}/{data.users.length}
+                        {t("legend.selected")} {selectedUserIds.size}/{data.users.length}
                       </span>
                     </div>
                   )}

+ 14 - 9
src/app/[locale]/dashboard/_components/statistics/wrapper.tsx

@@ -8,6 +8,7 @@ import type { TimeRange, UserStatisticsData } from "@/types/statistics";
 import type { CurrencyCode } from "@/lib/utils";
 import { DEFAULT_TIME_RANGE } from "@/types/statistics";
 import { toast } from "sonner";
+import { useTranslations } from "next-intl";
 
 interface StatisticsWrapperProps {
   initialData?: UserStatisticsData;
@@ -16,23 +17,27 @@ interface StatisticsWrapperProps {
 
 const STATISTICS_REFRESH_INTERVAL = 5000; // 5秒刷新一次
 
-async function fetchStatistics(timeRange: TimeRange): Promise<UserStatisticsData> {
-  const result = await getUserStatistics(timeRange);
-  if (!result.ok) {
-    throw new Error(result.error || "获取统计数据失败");
-  }
-  return result.data;
-}
-
 /**
  * 统计组件包装器
  * 处理时间范围状态管理和数据获取
  */
 export function StatisticsWrapper({ initialData, currencyCode = "USD" }: StatisticsWrapperProps) {
+  const t = useTranslations("dashboard.statistics");
   const [timeRange, setTimeRange] = React.useState<TimeRange>(
     initialData?.timeRange ?? DEFAULT_TIME_RANGE
   );
 
+  const fetchStatistics = React.useCallback(
+    async (timeRange: TimeRange): Promise<UserStatisticsData> => {
+      const result = await getUserStatistics(timeRange);
+      if (!result.ok) {
+        throw new Error(result.error || t("states.fetchFailed"));
+      }
+      return result.data;
+    },
+    [t]
+  );
+
   const { data, error } = useQuery<UserStatisticsData, Error>({
     queryKey: ["user-statistics", timeRange],
     queryFn: () => fetchStatistics(timeRange),
@@ -54,7 +59,7 @@ export function StatisticsWrapper({ initialData, currencyCode = "USD" }: Statist
 
   // 如果没有数据,显示空状态
   if (!data) {
-    return <div className="text-center py-8 text-muted-foreground">暂无统计数据</div>;
+    return <div className="text-center py-8 text-muted-foreground">{t("states.noData")}</div>;
   }
 
   return (

+ 3 - 1
src/app/[locale]/dashboard/_components/user-menu.tsx

@@ -4,6 +4,7 @@ import { useRouter } from "next/navigation";
 import { Avatar, AvatarFallback } from "@/components/ui/avatar";
 import { Button } from "@/components/ui/button";
 import { LogOut } from "lucide-react";
+import { useTranslations } from "next-intl";
 
 interface UserMenuProps {
   user: {
@@ -14,6 +15,7 @@ interface UserMenuProps {
 }
 
 export function UserMenu({ user }: UserMenuProps) {
+  const t = useTranslations("dashboard.nav");
   const router = useRouter();
 
   const handleLogout = () => {
@@ -49,7 +51,7 @@ export function UserMenu({ user }: UserMenuProps) {
         size="icon"
         onClick={handleLogout}
         className="h-9 w-9 rounded-full hover:bg-destructive/10 hover:text-destructive transition-all duration-200"
-        title="退出登录"
+        title={t("logout")}
       >
         <LogOut className="h-4 w-4" />
       </Button>

+ 3 - 0
src/app/[locale]/dashboard/layout.tsx

@@ -11,6 +11,9 @@ export default async function DashboardLayout({
   children: ReactNode;
   params: Promise<{ locale: string }>;
 }) {
+  // Await params to ensure locale is available in the async context
+  await params;
+
   const session = await getSession();
 
   if (!session) {

+ 5 - 2
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-table.tsx

@@ -11,6 +11,7 @@ import {
 import { Card, CardContent } from "@/components/ui/card";
 import { Badge } from "@/components/ui/badge";
 import { Trophy, Medal, Award } from "lucide-react";
+import { useTranslations } from "next-intl";
 
 // 支持动态列定义
 export interface ColumnDef<T> {
@@ -32,12 +33,14 @@ export function LeaderboardTable<T>({
   columns,
   getRowKey,
 }: LeaderboardTableProps<T>) {
+  const t = useTranslations("dashboard.leaderboard");
+
   if (data.length === 0) {
     return (
       <Card>
         <CardContent className="py-8">
           <div className="text-center text-muted-foreground">
-            {period === "daily" ? "今日" : "本月"}暂无数据
+            {t(period === "daily" ? "states.todayNoData" : "states.monthNoData")}
           </div>
         </CardContent>
       </Card>
@@ -84,7 +87,7 @@ export function LeaderboardTable<T>({
           <Table>
             <TableHeader>
               <TableRow>
-                <TableHead className="w-24">排名</TableHead>
+                <TableHead className="w-24">{t("columns.rank")}</TableHead>
                 {columns.map((col, idx) => (
                   <TableHead key={idx} className={col.className || ""}>
                     {col.header}

+ 20 - 18
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx

@@ -6,6 +6,7 @@ import { Card, CardContent } from "@/components/ui/card";
 import { LeaderboardTable, type ColumnDef } from "./leaderboard-table";
 import type { LeaderboardEntry, ProviderLeaderboardEntry } from "@/repository/leaderboard";
 import { formatTokenAmount } from "@/lib/utils";
+import { useTranslations } from "next-intl";
 
 interface LeaderboardViewProps {
   isAdmin: boolean;
@@ -15,6 +16,7 @@ type UserEntry = LeaderboardEntry & { totalCostFormatted?: string };
 type ProviderEntry = ProviderLeaderboardEntry & { totalCostFormatted?: string };
 
 export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
+  const t = useTranslations("dashboard.leaderboard");
   const [scope, setScope] = useState<"user" | "provider">("user");
   const [period, setPeriod] = useState<"daily" | "monthly">("daily");
   const [dailyData, setDailyData] = useState<Array<UserEntry | ProviderEntry>>([]);
@@ -33,7 +35,7 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
         ]);
 
         if (!dailyRes.ok || !monthlyRes.ok) {
-          throw new Error("获取排行榜数据失败");
+          throw new Error(t("states.fetchFailed"));
         }
 
         const [daily, monthly] = await Promise.all([dailyRes.json(), monthlyRes.json()]);
@@ -44,8 +46,8 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
           setError(null);
         }
       } catch (err) {
-        console.error("获取排行榜数据失败:", err);
-        if (!cancelled) setError(err instanceof Error ? err.message : "获取排行榜数据失败");
+        console.error(t("states.fetchFailed"), err);
+        if (!cancelled) setError(err instanceof Error ? err.message : t("states.fetchFailed"));
       } finally {
         if (!cancelled) setLoading(false);
       }
@@ -61,7 +63,7 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
     return (
       <Card>
         <CardContent className="py-8">
-          <div className="text-center text-muted-foreground">加载中...</div>
+          <div className="text-center text-muted-foreground">{t("states.loading")}</div>
         </CardContent>
       </Card>
     );
@@ -80,23 +82,23 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
   // 列定义(根据 scope 动态切换)
   const userColumns: ColumnDef<UserEntry>[] = [
     {
-      header: "用户",
+      header: t("columns.user"),
       cell: (row, index) => (
         <span className={index < 3 ? "font-semibold" : ""}>{(row as UserEntry).userName}</span>
       ),
     },
     {
-      header: "请求数",
+      header: t("columns.requests"),
       className: "text-right",
       cell: (row) => (row as UserEntry).totalRequests.toLocaleString(),
     },
     {
-      header: "Token 数",
+      header: t("columns.tokens"),
       className: "text-right",
       cell: (row) => formatTokenAmount((row as UserEntry).totalTokens),
     },
     {
-      header: "消耗金额",
+      header: t("columns.consumedAmount"),
       className: "text-right font-mono font-semibold",
       cell: (row) => {
         const r = row as UserEntry & { totalCostFormatted?: string };
@@ -107,7 +109,7 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
 
   const providerColumns: ColumnDef<ProviderEntry>[] = [
     {
-      header: "供应商",
+      header: t("columns.provider"),
       cell: (row, index) => (
         <span className={index < 3 ? "font-semibold" : ""}>
           {(row as ProviderEntry).providerName}
@@ -115,12 +117,12 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
       ),
     },
     {
-      header: "请求数",
+      header: t("columns.requests"),
       className: "text-right",
       cell: (row) => (row as ProviderEntry).totalRequests.toLocaleString(),
     },
     {
-      header: "成本",
+      header: t("columns.cost"),
       className: "text-right font-mono font-semibold",
       cell: (row) => {
         const r = row as ProviderEntry & { totalCostFormatted?: string };
@@ -128,17 +130,17 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
       },
     },
     {
-      header: "Token 数",
+      header: t("columns.tokens"),
       className: "text-right",
       cell: (row) => formatTokenAmount((row as ProviderEntry).totalTokens),
     },
     {
-      header: "成功率",
+      header: t("columns.successRate"),
       className: "text-right",
       cell: (row) => `${(((row as ProviderEntry).successRate || 0) * 100).toFixed(1)}%`,
     },
     {
-      header: "平均响应时间",
+      header: t("columns.avgResponseTime"),
       className: "text-right",
       cell: (row) =>
         `${Math.round((row as ProviderEntry).avgResponseTime || 0).toLocaleString()} ms`,
@@ -161,15 +163,15 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
       <div className="flex flex-wrap gap-4 items-center">
         <Tabs value={scope} onValueChange={(v) => setScope(v as typeof scope)}>
           <TabsList className={isAdmin ? "grid grid-cols-2" : ""}>
-            <TabsTrigger value="user">用户排行</TabsTrigger>
-            {isAdmin && <TabsTrigger value="provider">供应商排行</TabsTrigger>}
+            <TabsTrigger value="user">{t("tabs.userRanking")}</TabsTrigger>
+            {isAdmin && <TabsTrigger value="provider">{t("tabs.providerRanking")}</TabsTrigger>}
           </TabsList>
         </Tabs>
 
         <Tabs value={period} onValueChange={(v) => setPeriod(v as typeof period)}>
           <TabsList className="grid grid-cols-2">
-            <TabsTrigger value="daily">今日排行</TabsTrigger>
-            <TabsTrigger value="monthly">本月排行</TabsTrigger>
+            <TabsTrigger value="daily">{t("tabs.dailyRanking")}</TabsTrigger>
+            <TabsTrigger value="monthly">{t("tabs.monthlyRanking")}</TabsTrigger>
           </TabsList>
         </Tabs>
       </div>

+ 11 - 9
src/app/[locale]/dashboard/leaderboard/page.tsx

@@ -6,10 +6,12 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { AlertCircle } from "lucide-react";
 import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
 import { Link } from "@/i18n/routing";
+import { getTranslations } from "next-intl/server";
 
 export const dynamic = "force-dynamic";
 
 export default async function LeaderboardPage() {
+  const t = await getTranslations("dashboard");
   // 获取用户 session 和系统设置
   const session = await getSession();
   const systemSettings = await getSystemSettings();
@@ -22,30 +24,30 @@ export default async function LeaderboardPage() {
   if (!hasPermission) {
     return (
       <div className="space-y-6">
-        <Section title="消耗排行榜" description="查看用户消耗排名,数据每 5 分钟更新一次">
+        <Section title={t("title.costRanking")} description={t("title.costRankingDescription")}>
           <Card>
             <CardHeader>
               <CardTitle className="flex items-center gap-2">
                 <AlertCircle className="h-5 w-5 text-muted-foreground" />
-                需要权限
+                {t("leaderboard.permission.title")}
               </CardTitle>
             </CardHeader>
             <CardContent className="space-y-4">
               <Alert>
                 <AlertCircle className="h-4 w-4" />
-                <AlertTitle>访问受限</AlertTitle>
+                <AlertTitle>{t("leaderboard.permission.restricted")}</AlertTitle>
                 <AlertDescription>
-                  排行榜功能需要管理员开启&nbsp;&quot;允许查看全站使用量&quot;&nbsp;权限。
+                  {t("leaderboard.permission.description")}
                   {isAdmin && (
                     <span>
-                      请前往{" "}
+                      {" "}
                       <Link href="/settings/config" className="underline font-medium">
-                        系统设置
+                        {t("leaderboard.permission.systemSettings")}
                       </Link>{" "}
-                      开启此权限。
+                      {t("leaderboard.permission.adminAction")}
                     </span>
                   )}
-                  {!isAdmin && <span>请联系管理员开启此权限。</span>}
+                  {!isAdmin && <span> {t("leaderboard.permission.userAction")}</span>}
                 </AlertDescription>
               </Alert>
             </CardContent>
@@ -58,7 +60,7 @@ export default async function LeaderboardPage() {
   // 有权限时渲染排行榜
   return (
     <div className="space-y-6">
-      <Section title="消耗排行榜" description="查看用户消耗排名,数据每 5 分钟更新一次">
+      <Section title={t("title.costRanking")} description={t("title.costRankingDescription")}>
         <LeaderboardView isAdmin={isAdmin} />
       </Section>
     </div>

+ 6 - 1
src/app/[locale]/dashboard/logs/page.tsx

@@ -1,5 +1,5 @@
 import { Suspense } from "react";
-import { redirect } from "next/navigation";
+import { redirect } from "@/i18n/routing";
 import { getSession } from "@/lib/auth";
 import { Section } from "@/components/section";
 import { UsageLogsView } from "./_components/usage-logs-view";
@@ -12,10 +12,15 @@ import { getSystemSettings } from "@/repository/system-config";
 export const dynamic = "force-dynamic";
 
 export default async function UsageLogsPage({
+  params,
   searchParams,
 }: {
+  params: Promise<{ locale: string }>;
   searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
 }) {
+  // Await params to ensure locale is available in the async context
+  await params;
+
   const session = await getSession();
   if (!session) {
     redirect("/login");

+ 12 - 2
src/app/[locale]/dashboard/page.tsx

@@ -10,10 +10,20 @@ import { ListErrorBoundary } from "@/components/error-boundary";
 import { StatisticsWrapper } from "./_components/statistics";
 import { OverviewPanel } from "@/components/customs/overview-panel";
 import { DEFAULT_TIME_RANGE } from "@/types/statistics";
+import { getTranslations } from "next-intl/server";
 
 export const dynamic = "force-dynamic";
 
-export default async function DashboardPage() {
+export default async function DashboardPage({
+  params,
+}: {
+  params: Promise<{ locale: string }>;
+}) {
+  // Await params to ensure locale is available in the async context
+  await params;
+
+  const t = await getTranslations("dashboard");
+
   // 检查价格表是否存在,如果不存在则跳转到价格上传页面
   const hasPrices = await hasPriceTable();
   if (!hasPrices) {
@@ -41,7 +51,7 @@ export default async function DashboardPage() {
         />
       </div>
 
-      <Section title="客户端" description="用户和密钥管理">
+      <Section title={t("title.clients")} description={t("title.userAndKeyManagement")}>
         <ListErrorBoundary>
           <UserKeyManager
             users={users}

+ 4 - 2
src/app/[locale]/dashboard/quotas/keys/page.tsx

@@ -2,6 +2,7 @@ import { getUsers, getUserLimitUsage } from "@/actions/users";
 import { getKeyLimitUsage } from "@/actions/keys";
 import { KeysQuotaManager } from "./_components/keys-quota-manager";
 import { getSystemSettings } from "@/repository/system-config";
+import { getTranslations } from "next-intl/server";
 
 async function getUsersWithKeysQuotas() {
   const users = await getUsers();
@@ -45,14 +46,15 @@ export default async function KeysQuotaPage() {
     getSystemSettings(),
   ]);
   const totalKeys = users.reduce((sum, user) => sum + user.keys.length, 0);
+  const t = await getTranslations("quota.keys");
 
   return (
     <div className="space-y-4">
       <div className="flex items-center justify-between">
         <div>
-          <h3 className="text-lg font-medium">密钥限额统计</h3>
+          <h3 className="text-lg font-medium">{t("title")}</h3>
           <p className="text-sm text-muted-foreground">
-            共 {users.length} 个用户,{totalKeys} 个密钥
+            {t("totalCount", { users: users.length, keys: totalKeys })}
           </p>
         </div>
       </div>

+ 9 - 5
src/app/[locale]/dashboard/quotas/layout.tsx

@@ -1,5 +1,7 @@
 import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
 import { Link } from "@/i18n/routing";
+import { useTranslations } from "next-intl";
+import { getTranslations } from "next-intl/server";
 
 export default async function QuotasLayout({
   children,
@@ -8,23 +10,25 @@ export default async function QuotasLayout({
   children: React.ReactNode;
   params: Promise<{ locale: string }>;
 }) {
+  const t = await getTranslations("quota.layout");
+
   return (
     <div className="space-y-6">
       <div>
-        <h2 className="text-3xl font-bold tracking-tight">限额管理</h2>
-        <p className="text-muted-foreground">查看和管理所有层级的限额使用情况</p>
+        <h2 className="text-3xl font-bold tracking-tight">{t("title")}</h2>
+        <p className="text-muted-foreground">{t("description")}</p>
       </div>
 
       <Tabs defaultValue="users" className="space-y-4">
         <TabsList>
           <Link href="/dashboard/quotas/users">
-            <TabsTrigger value="users">用户限额</TabsTrigger>
+            <TabsTrigger value="users">{t("tabs.users")}</TabsTrigger>
           </Link>
           <Link href="/dashboard/quotas/keys">
-            <TabsTrigger value="keys">密钥限额</TabsTrigger>
+            <TabsTrigger value="keys">{t("tabs.keys")}</TabsTrigger>
           </Link>
           <Link href="/dashboard/quotas/providers">
-            <TabsTrigger value="providers">供应商限额</TabsTrigger>
+            <TabsTrigger value="providers">{t("tabs.providers")}</TabsTrigger>
           </Link>
         </TabsList>
 

+ 7 - 1
src/app/[locale]/dashboard/quotas/page.tsx

@@ -1,5 +1,11 @@
 import { redirect } from "@/i18n/routing";
 
-export default function QuotasPage() {
+export default async function QuotasPage({
+  params,
+}: {
+  params: Promise<{ locale: string }>;
+}) {
+  // Await params to ensure locale is available in the async context
+  await params;
   redirect("/dashboard/quotas/users" as any);
 }

+ 14 - 13
src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-client.tsx

@@ -8,7 +8,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/component
 import { ChevronDown } from "lucide-react";
 import { formatCurrency, type CurrencyCode } from "@/lib/utils/currency";
 import { formatDateDistance } from "@/lib/utils/date-format";
-import { useLocale } from "next-intl";
+import { useLocale, useTranslations } from "next-intl";
 import type { ProviderType } from "@/types/provider";
 
 interface ProviderQuota {
@@ -53,6 +53,7 @@ export function ProvidersQuotaClient({
   // 折叠状态
   const [isUnlimitedOpen, setIsUnlimitedOpen] = useState(false);
   const locale = useLocale();
+  const t = useTranslations("quota.providers");
 
   // 筛选、排序和分组供应商
   const { providersWithQuota, providersWithoutQuota } = useMemo(() => {
@@ -99,13 +100,13 @@ export function ProvidersQuotaClient({
           <CardTitle className="text-base">{provider.name}</CardTitle>
           <div className="flex gap-2">
             <Badge variant={provider.isEnabled ? "default" : "secondary"}>
-              {provider.isEnabled ? "启用" : "禁用"}
+              {provider.isEnabled ? t("status.enabled") : t("status.disabled")}
             </Badge>
             <Badge variant="outline">{provider.providerType}</Badge>
           </div>
         </div>
         <CardDescription>
-          优先级: {provider.priority} · 权重: {provider.weight}
+          {t("card.priority")}: {provider.priority} · {t("card.weight")}: {provider.weight}
         </CardDescription>
       </CardHeader>
       <CardContent className="space-y-4">
@@ -115,7 +116,7 @@ export function ProvidersQuotaClient({
             {provider.quota.cost5h.limit && provider.quota.cost5h.limit > 0 && (
               <div className="space-y-2">
                 <div className="flex items-center justify-between text-sm">
-                  <span className="text-muted-foreground">5小时消费</span>
+                  <span className="text-muted-foreground">{t("cost5h.label")}</span>
                   <span className="font-medium">
                     {formatCurrency(provider.quota.cost5h.current, currencyCode)} /{" "}
                     {formatCurrency(provider.quota.cost5h.limit, currencyCode)}
@@ -133,7 +134,7 @@ export function ProvidersQuotaClient({
             {provider.quota.costWeekly.limit && provider.quota.costWeekly.limit > 0 && (
               <div className="space-y-2">
                 <div className="flex items-center justify-between text-sm">
-                  <span className="text-muted-foreground">周消费</span>
+                  <span className="text-muted-foreground">{t("costWeekly.label")}</span>
                   <span className="font-medium">
                     {formatCurrency(provider.quota.costWeekly.current, currencyCode)} /{" "}
                     {formatCurrency(provider.quota.costWeekly.limit, currencyCode)}
@@ -147,7 +148,7 @@ export function ProvidersQuotaClient({
                   className="h-2"
                 />
                 <p className="text-xs text-muted-foreground">
-                  重置于{" "}
+                  {t("costWeekly.resetAt")}{" "}
                   {formatDateDistance(
                     new Date(provider.quota.costWeekly.resetAt),
                     new Date(),
@@ -161,7 +162,7 @@ export function ProvidersQuotaClient({
             {provider.quota.costMonthly.limit && provider.quota.costMonthly.limit > 0 && (
               <div className="space-y-2">
                 <div className="flex items-center justify-between text-sm">
-                  <span className="text-muted-foreground">月消费</span>
+                  <span className="text-muted-foreground">{t("costMonthly.label")}</span>
                   <span className="font-medium">
                     {formatCurrency(provider.quota.costMonthly.current, currencyCode)} /{" "}
                     {formatCurrency(provider.quota.costMonthly.limit, currencyCode)}
@@ -175,7 +176,7 @@ export function ProvidersQuotaClient({
                   className="h-2"
                 />
                 <p className="text-xs text-muted-foreground">
-                  重置于{" "}
+                  {t("costMonthly.resetAt")}{" "}
                   {formatDateDistance(
                     new Date(provider.quota.costMonthly.resetAt),
                     new Date(),
@@ -189,7 +190,7 @@ export function ProvidersQuotaClient({
             {provider.quota.concurrentSessions.limit > 0 && (
               <div className="space-y-2">
                 <div className="flex items-center justify-between text-sm">
-                  <span className="text-muted-foreground">并发 Session</span>
+                  <span className="text-muted-foreground">{t("concurrentSessions.label")}</span>
                   <span className="font-medium">
                     {provider.quota.concurrentSessions.current} /{" "}
                     {provider.quota.concurrentSessions.limit}
@@ -207,11 +208,11 @@ export function ProvidersQuotaClient({
             )}
 
             {!hasQuotaLimit(provider.quota) && (
-              <p className="text-sm text-muted-foreground">未设置限额</p>
+              <p className="text-sm text-muted-foreground">{t("noQuotaSet")}</p>
             )}
           </>
         ) : (
-          <p className="text-sm text-muted-foreground">无法获取限额信息</p>
+          <p className="text-sm text-muted-foreground">{t("noQuotaData")}</p>
         )}
       </CardContent>
     </Card>
@@ -224,7 +225,7 @@ export function ProvidersQuotaClient({
       {totalProviders === 0 ? (
         <Card>
           <CardContent className="flex items-center justify-center py-10">
-            <p className="text-muted-foreground">没有匹配的供应商</p>
+            <p className="text-muted-foreground">{t("noMatches")}</p>
           </CardContent>
         </Card>
       ) : (
@@ -241,7 +242,7 @@ export function ProvidersQuotaClient({
             <Collapsible open={isUnlimitedOpen} onOpenChange={setIsUnlimitedOpen}>
               <CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg border bg-card p-4 text-sm font-medium hover:bg-accent">
                 <span className="text-muted-foreground">
-                  未设置限额的供应商 ({providersWithoutQuota.length}个)
+                  {t("unlimitedSection", { count: providersWithoutQuota.length })}
                 </span>
                 <ChevronDown
                   className={`h-4 w-4 transition-transform ${isUnlimitedOpen ? "rotate-180" : ""}`}

+ 3 - 1
src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-manager.tsx

@@ -5,6 +5,7 @@ import { ProviderTypeFilter } from "@/app/[locale]/settings/providers/_component
 import { ProvidersQuotaClient } from "./providers-quota-client";
 import type { ProviderType } from "@/types/provider";
 import type { CurrencyCode } from "@/lib/utils/currency";
+import { useTranslations } from "next-intl";
 
 interface ProviderQuota {
   cost5h: { current: number; limit: number | null; resetInfo: string };
@@ -33,6 +34,7 @@ export function ProvidersQuotaManager({
   currencyCode = "USD",
 }: ProvidersQuotaManagerProps) {
   const [typeFilter, setTypeFilter] = useState<ProviderType | "all">("all");
+  const t = useTranslations("quota.providers");
 
   // 计算筛选后的供应商数量
   const filteredCount =
@@ -46,7 +48,7 @@ export function ProvidersQuotaManager({
       <div className="flex items-center justify-between">
         <ProviderTypeFilter value={typeFilter} onChange={setTypeFilter} />
         <div className="text-sm text-muted-foreground">
-          显示 {filteredCount} / {providers.length} 个供应商
+          {t("filterCount", { filtered: filteredCount, total: providers.length })}
         </div>
       </div>
 

+ 4 - 2
src/app/[locale]/dashboard/quotas/providers/page.tsx

@@ -2,6 +2,7 @@ import { getProviders } from "@/actions/providers";
 import { getProviderLimitUsage } from "@/actions/providers";
 import { ProvidersQuotaManager } from "./_components/providers-quota-manager";
 import { getSystemSettings } from "@/repository/system-config";
+import { getTranslations } from "next-intl/server";
 
 async function getProvidersWithQuotas() {
   const providers = await getProviders();
@@ -29,13 +30,14 @@ export default async function ProvidersQuotaPage() {
     getProvidersWithQuotas(),
     getSystemSettings(),
   ]);
+  const t = await getTranslations("quota.providers");
 
   return (
     <div className="space-y-4">
       <div className="flex items-center justify-between">
         <div>
-          <h3 className="text-lg font-medium">供应商限额统计</h3>
-          <p className="text-sm text-muted-foreground">共 {providers.length} 个供应商</p>
+          <h3 className="text-lg font-medium">{t("title")}</h3>
+          <p className="text-sm text-muted-foreground">{t("totalCount", { count: providers.length })}</p>
         </div>
       </div>
 

+ 9 - 8
src/app/[locale]/dashboard/quotas/users/_components/users-quota-client.tsx

@@ -6,7 +6,7 @@ import { Badge } from "@/components/ui/badge";
 import { QuotaProgress } from "@/components/quota/quota-progress";
 import { formatCurrency, type CurrencyCode } from "@/lib/utils/currency";
 import { formatDateDistance } from "@/lib/utils/date-format";
-import { useLocale } from "next-intl";
+import { useLocale, useTranslations } from "next-intl";
 import { Progress } from "@/components/ui/progress";
 
 interface UserQuota {
@@ -38,6 +38,7 @@ export function UsersQuotaClient({
   filter = "all",
 }: UsersQuotaClientProps) {
   const locale = useLocale();
+  const t = useTranslations("quota.users");
 
   // 计算使用率(用于排序和筛选)
   const usersWithUsage = useMemo(() => {
@@ -89,7 +90,7 @@ export function UsersQuotaClient({
                 <CardTitle className="text-base">{user.name}</CardTitle>
                 <Badge variant={user.role === "admin" ? "default" : "secondary"}>{user.role}</Badge>
               </div>
-              <CardDescription>{user.note || "无备注"}</CardDescription>
+              <CardDescription>{user.note || t("noNote")}</CardDescription>
             </CardHeader>
             <CardContent className="space-y-4">
               {user.quota ? (
@@ -97,7 +98,7 @@ export function UsersQuotaClient({
                   {/* RPM 限额 */}
                   <div className="space-y-2">
                     <div className="flex items-center justify-between text-sm">
-                      <span className="text-muted-foreground">RPM 限额</span>
+                      <span className="text-muted-foreground">{t("rpm.label")}</span>
                       <span className="font-medium">
                         {user.quota.rpm.current} / {user.quota.rpm.limit}
                       </span>
@@ -110,13 +111,13 @@ export function UsersQuotaClient({
                       }
                       className="h-2"
                     />
-                    <p className="text-xs text-muted-foreground">每分钟请求数</p>
+                    <p className="text-xs text-muted-foreground">{t("rpm.description")}</p>
                   </div>
 
                   {/* 每日消费限额 */}
                   <div className="space-y-2">
                     <div className="flex items-center justify-between text-sm">
-                      <span className="text-muted-foreground">每日消费</span>
+                      <span className="text-muted-foreground">{t("dailyCost.label")}</span>
                       <span className="font-medium">
                         {formatCurrency(user.quota.dailyCost.current, currencyCode)} /{" "}
                         {formatCurrency(user.quota.dailyCost.limit, currencyCode)}
@@ -127,7 +128,7 @@ export function UsersQuotaClient({
                       limit={user.quota.dailyCost.limit}
                     />
                     <p className="text-xs text-muted-foreground">
-                      重置于{" "}
+                      {t("dailyCost.resetAt")}{" "}
                       {formatDateDistance(
                         new Date(user.quota.dailyCost.resetAt),
                         new Date(),
@@ -137,7 +138,7 @@ export function UsersQuotaClient({
                   </div>
                 </>
               ) : (
-                <p className="text-sm text-muted-foreground">无法获取限额信息</p>
+                <p className="text-sm text-muted-foreground">{t("noQuotaData")}</p>
               )}
             </CardContent>
           </Card>
@@ -148,7 +149,7 @@ export function UsersQuotaClient({
         <Card>
           <CardContent className="flex items-center justify-center py-10">
             <p className="text-muted-foreground">
-              {searchQuery ? "未找到匹配的用户" : "暂无用户数据"}
+              {searchQuery ? t("noMatches") : t("noData")}
             </p>
           </CardContent>
         </Card>

+ 9 - 7
src/app/[locale]/dashboard/quotas/users/page.tsx

@@ -3,6 +3,7 @@ import { getUserLimitUsage } from "@/actions/users";
 import { QuotaToolbar } from "@/components/quota/quota-toolbar";
 import { UsersQuotaClient } from "./_components/users-quota-client";
 import { getSystemSettings } from "@/repository/system-config";
+import { getTranslations } from "next-intl/server";
 
 async function getUsersWithQuotas() {
   const users = await getUsers();
@@ -25,25 +26,26 @@ async function getUsersWithQuotas() {
 
 export default async function UsersQuotaPage() {
   const [users, systemSettings] = await Promise.all([getUsersWithQuotas(), getSystemSettings()]);
+  const t = await getTranslations("quota.users");
 
   return (
     <div className="space-y-4">
       <div className="flex items-center justify-between">
         <div>
-          <h3 className="text-lg font-medium">用户限额统计</h3>
-          <p className="text-sm text-muted-foreground">共 {users.length} 个用户</p>
+          <h3 className="text-lg font-medium">{t("title")}</h3>
+          <p className="text-sm text-muted-foreground">{t("totalCount", { count: users.length })}</p>
         </div>
       </div>
 
       <QuotaToolbar
         sortOptions={[
-          { value: "name", label: "按名称" },
-          { value: "usage", label: "按使用率" },
+          { value: "name", label: t("sort.name") },
+          { value: "usage", label: t("sort.usage") },
         ]}
         filterOptions={[
-          { value: "all", label: "全部" },
-          { value: "warning", label: "接近限额 (>60%)" },
-          { value: "exceeded", label: "已超限 (≥100%)" },
+          { value: "all", label: t("filter.all") },
+          { value: "warning", label: t("filter.warning") },
+          { value: "exceeded", label: t("filter.exceeded") },
         ]}
       />
 

+ 62 - 60
src/app/[locale]/internal/data-gen/_components/data-generator-page.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 import { useState, useMemo } from "react";
+import { useTranslations } from "next-intl";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
@@ -20,10 +21,11 @@ import { AlertCircle, Download, FileDown, Loader2, Settings } from "lucide-react
 import type { GeneratorResult, UserBreakdownResult } from "@/lib/data-generator/types";
 
 export function DataGeneratorPage() {
+  const t = useTranslations("internal.dataGenerator");
   const [mode, setMode] = useState<"usage" | "userBreakdown">("usage");
   const [startDate, setStartDate] = useState<string>("");
   const [endDate, setEndDate] = useState<string>("");
-  const [serviceName, setServiceName] = useState<string>("AI大模型推理服务");
+  const [serviceName, setServiceName] = useState<string>("");
   const [totalCostCny, setTotalCostCny] = useState<string>("");
   const [totalRecords, setTotalRecords] = useState<string>("");
   const [models, setModels] = useState<string>("");
@@ -87,7 +89,7 @@ export function DataGeneratorPage() {
 
       if (!response.ok) {
         const data = await response.json();
-        throw new Error(data.error || "Failed to generate logs");
+        throw new Error(data.error || t("errors.failed"));
       }
 
       if (mode === "userBreakdown") {
@@ -198,8 +200,8 @@ export function DataGeneratorPage() {
     <div className="space-y-6 p-6">
       <Tabs value={mode} onValueChange={(v) => setMode(v as "usage" | "userBreakdown")}>
         <TabsList>
-          <TabsTrigger value="usage">用量数据生成</TabsTrigger>
-          <TabsTrigger value="userBreakdown">按用户显示用量</TabsTrigger>
+          <TabsTrigger value="usage">{t("tabs.usage")}</TabsTrigger>
+          <TabsTrigger value="userBreakdown">{t("tabs.userBreakdown")}</TabsTrigger>
         </TabsList>
       </Tabs>
 
@@ -207,7 +209,7 @@ export function DataGeneratorPage() {
         <div className="flex justify-end">
           <Button variant="outline" size="sm" onClick={() => setShowParams(true)}>
             <Settings className="mr-2 h-4 w-4" />
-            重新配置参数
+            {t("actions.reconfigure")}
           </Button>
         </div>
       )}
@@ -215,13 +217,13 @@ export function DataGeneratorPage() {
       {showParams && (
         <Card>
           <CardHeader>
-            <CardTitle>生成参数</CardTitle>
-            <CardDescription>配置生成参数以创建模拟日志数据</CardDescription>
+            <CardTitle>{t("params.title")}</CardTitle>
+            <CardDescription>{t("params.description")}</CardDescription>
           </CardHeader>
           <CardContent className="space-y-4">
             <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
               <div className="space-y-2">
-                <Label htmlFor="startDate">起始时间 *</Label>
+                <Label htmlFor="startDate">{t("params.startDate")} {t("params.required")}</Label>
                 <Input
                   id="startDate"
                   type="datetime-local"
@@ -230,7 +232,7 @@ export function DataGeneratorPage() {
                 />
               </div>
               <div className="space-y-2">
-                <Label htmlFor="endDate">结束时间 *</Label>
+                <Label htmlFor="endDate">{t("params.endDate")} {t("params.required")}</Label>
                 <Input
                   id="endDate"
                   type="datetime-local"
@@ -239,58 +241,58 @@ export function DataGeneratorPage() {
                 />
               </div>
               <div className="space-y-2">
-                <Label htmlFor="totalCostCny">总金额(人民币)</Label>
+                <Label htmlFor="totalCostCny">{t("params.totalCostCny")}</Label>
                 <Input
                   id="totalCostCny"
                   type="number"
-                  placeholder="如:1000"
+                  placeholder={t("params.placeholders.totalCostCny")}
                   value={totalCostCny}
                   onChange={(e) => setTotalCostCny(e.target.value)}
                 />
               </div>
               <div className="space-y-2">
-                <Label htmlFor="totalRecords">总记录数</Label>
+                <Label htmlFor="totalRecords">{t("params.totalRecords")}</Label>
                 <Input
                   id="totalRecords"
                   type="number"
-                  placeholder="如:500(不填则根据金额计算)"
+                  placeholder={t("params.placeholders.totalRecords")}
                   value={totalRecords}
                   onChange={(e) => setTotalRecords(e.target.value)}
                 />
               </div>
               <div className="space-y-2">
-                <Label htmlFor="models">包含模型(逗号分隔)</Label>
+                <Label htmlFor="models">{t("params.models")}</Label>
                 <Input
                   id="models"
-                  placeholder="如:claude-3-5-sonnet,gpt-4(留空则全部)"
+                  placeholder={t("params.placeholders.models")}
                   value={models}
                   onChange={(e) => setModels(e.target.value)}
                 />
               </div>
               <div className="space-y-2">
-                <Label htmlFor="userIds">用户ID(逗号分隔)</Label>
+                <Label htmlFor="userIds">{t("params.userIds")}</Label>
                 <Input
                   id="userIds"
-                  placeholder="如:1,2,3(留空则全部)"
+                  placeholder={t("params.placeholders.userIds")}
                   value={userIds}
                   onChange={(e) => setUserIds(e.target.value)}
                 />
               </div>
               <div className="space-y-2">
-                <Label htmlFor="providerIds">供应商ID(逗号分隔)</Label>
+                <Label htmlFor="providerIds">{t("params.providerIds")}</Label>
                 <Input
                   id="providerIds"
-                  placeholder="如:1,2(留空则全部)"
+                  placeholder={t("params.placeholders.providerIds")}
                   value={providerIds}
                   onChange={(e) => setProviderIds(e.target.value)}
                 />
               </div>
               {mode === "userBreakdown" && (
                 <div className="space-y-2">
-                  <Label htmlFor="serviceName">服务名称</Label>
+                  <Label htmlFor="serviceName">{t("params.serviceName")}</Label>
                   <Input
                     id="serviceName"
-                    placeholder="AI大模型推理服务"
+                    placeholder={t("params.placeholders.serviceName")}
                     value={serviceName}
                     onChange={(e) => setServiceName(e.target.value)}
                   />
@@ -300,7 +302,7 @@ export function DataGeneratorPage() {
 
             <Button onClick={handleGenerate} disabled={loading || !startDate || !endDate}>
               {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
-              生成数据
+              {t("actions.generate")}
             </Button>
           </CardContent>
         </Card>
@@ -318,18 +320,18 @@ export function DataGeneratorPage() {
           <div className="flex justify-end gap-2">
             <Button variant="outline" size="sm" onClick={handleExportScreenshot}>
               <Download className="mr-2 h-4 w-4" />
-              导出截图
+              {t("actions.exportScreenshot")}
             </Button>
             <Button variant="outline" size="sm" onClick={handleExportPDF}>
               <FileDown className="mr-2 h-4 w-4" />
-              导出 PDF
+              {t("actions.exportPDF")}
             </Button>
           </div>
 
           <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
             <Card>
               <CardHeader className="pb-2">
-                <CardDescription>总记录数</CardDescription>
+                <CardDescription>{t("summary.totalRecords")}</CardDescription>
                 <CardTitle className="text-2xl">
                   {result.summary.totalRecords.toLocaleString()}
                 </CardTitle>
@@ -337,13 +339,13 @@ export function DataGeneratorPage() {
             </Card>
             {/* <Card>
               <CardHeader className="pb-2">
-                <CardDescription>总成本</CardDescription>
+                <CardDescription>{t("summary.totalCost")}</CardDescription>
                 <CardTitle className="text-2xl">${result.summary.totalCost.toFixed(4)}</CardTitle>
               </CardHeader>
             </Card> */}
             <Card>
               <CardHeader className="pb-2">
-                <CardDescription>总成本(人民币)</CardDescription>
+                <CardDescription>{t("summary.totalCostCny")}</CardDescription>
                 <CardTitle className="text-2xl">
                   ¥{(result.summary.totalCost * 7.1).toFixed(2)}
                 </CardTitle>
@@ -351,7 +353,7 @@ export function DataGeneratorPage() {
             </Card>
             <Card>
               <CardHeader className="pb-2">
-                <CardDescription>总 Token</CardDescription>
+                <CardDescription>{t("summary.totalTokens")}</CardDescription>
                 <CardTitle className="text-2xl">
                   {result.summary.totalTokens.toLocaleString()}
                 </CardTitle>
@@ -361,26 +363,26 @@ export function DataGeneratorPage() {
 
           <Card>
             <CardHeader>
-              <CardTitle>使用日志</CardTitle>
-              <CardDescription>共 {result.logs.length} 条记录</CardDescription>
+              <CardTitle>{t("table.usageLogs.title")}</CardTitle>
+              <CardDescription>{t("table.usageLogs.description", { count: result.logs.length })}</CardDescription>
             </CardHeader>
             <CardContent>
               <div className="rounded-md border max-h-[600px] overflow-auto">
                 <Table>
                   <TableHeader className="sticky top-0 bg-background">
                     <TableRow>
-                      <TableHead>时间</TableHead>
-                      <TableHead>用户</TableHead>
-                      <TableHead>密钥</TableHead>
-                      <TableHead>供应商</TableHead>
-                      <TableHead>模型</TableHead>
-                      <TableHead className="text-right">输入</TableHead>
-                      <TableHead className="text-right">输出</TableHead>
-                      <TableHead className="text-right">缓存写</TableHead>
-                      <TableHead className="text-right">缓存读</TableHead>
-                      <TableHead className="text-right">成本</TableHead>
-                      <TableHead className="text-right">耗时</TableHead>
-                      <TableHead>状态</TableHead>
+                      <TableHead>{t("table.usageLogs.columns.time")}</TableHead>
+                      <TableHead>{t("table.usageLogs.columns.user")}</TableHead>
+                      <TableHead>{t("table.usageLogs.columns.key")}</TableHead>
+                      <TableHead>{t("table.usageLogs.columns.provider")}</TableHead>
+                      <TableHead>{t("table.usageLogs.columns.model")}</TableHead>
+                      <TableHead className="text-right">{t("table.usageLogs.columns.input")}</TableHead>
+                      <TableHead className="text-right">{t("table.usageLogs.columns.output")}</TableHead>
+                      <TableHead className="text-right">{t("table.usageLogs.columns.cacheWrite")}</TableHead>
+                      <TableHead className="text-right">{t("table.usageLogs.columns.cacheRead")}</TableHead>
+                      <TableHead className="text-right">{t("table.usageLogs.columns.cost")}</TableHead>
+                      <TableHead className="text-right">{t("table.usageLogs.columns.duration")}</TableHead>
+                      <TableHead>{t("table.usageLogs.columns.status")}</TableHead>
                     </TableRow>
                   </TableHeader>
                   <TableBody>
@@ -420,7 +422,7 @@ export function DataGeneratorPage() {
                         <TableCell>
                           {log.statusCode === 200 ? (
                             <span className="inline-flex items-center rounded-md bg-green-100 dark:bg-green-950 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-300">
-                              成功
+                              {t("status.success")}
                             </span>
                           ) : (
                             <span className="inline-flex items-center rounded-md bg-red-100 dark:bg-red-950 px-2 py-1 text-xs font-medium text-red-700 dark:text-red-300">
@@ -443,18 +445,18 @@ export function DataGeneratorPage() {
           <div className="flex justify-end gap-2">
             <Button variant="outline" size="sm" onClick={handleExportScreenshot}>
               <Download className="mr-2 h-4 w-4" />
-              导出截图
+              {t("actions.exportScreenshot")}
             </Button>
             <Button variant="outline" size="sm" onClick={handleExportPDF}>
               <FileDown className="mr-2 h-4 w-4" />
-              导出 PDF
+              {t("actions.exportPDF")}
             </Button>
           </div>
 
           <div className="grid grid-cols-1 md:grid-cols-5 gap-4">
             <Card>
               <CardHeader className="pb-2">
-                <CardDescription>时间范围</CardDescription>
+                <CardDescription>{t("summary.timeRange")}</CardDescription>
                 <CardTitle className="text-sm">
                   <div>
                     {new Date(startDate).toLocaleString("zh-CN", {
@@ -464,7 +466,7 @@ export function DataGeneratorPage() {
                       minute: "2-digit",
                     })}
                   </div>
-                  <div className="text-muted-foreground text-xs"></div>
+                  <div className="text-muted-foreground text-xs">{t("summary.to")}</div>
                   <div>
                     {new Date(endDate).toLocaleString("zh-CN", {
                       month: "numeric",
@@ -478,7 +480,7 @@ export function DataGeneratorPage() {
             </Card>
             <Card>
               <CardHeader className="pb-2">
-                <CardDescription>总用户数</CardDescription>
+                <CardDescription>{t("summary.uniqueUsers")}</CardDescription>
                 <CardTitle className="text-2xl">
                   {userBreakdownResult.summary.uniqueUsers.toLocaleString()}
                 </CardTitle>
@@ -486,7 +488,7 @@ export function DataGeneratorPage() {
             </Card>
             <Card>
               <CardHeader className="pb-2">
-                <CardDescription>总调用数</CardDescription>
+                <CardDescription>{t("summary.totalCalls")}</CardDescription>
                 <CardTitle className="text-2xl">
                   {userBreakdownResult.summary.totalCalls.toLocaleString()}
                 </CardTitle>
@@ -494,7 +496,7 @@ export function DataGeneratorPage() {
             </Card>
             <Card>
               <CardHeader className="pb-2">
-                <CardDescription>总成本(人民币)</CardDescription>
+                <CardDescription>{t("summary.totalCostCny")}</CardDescription>
                 <CardTitle className="text-2xl">
                   ¥{(userBreakdownResult.summary.totalCost * 7.1).toFixed(2)}
                 </CardTitle>
@@ -506,11 +508,11 @@ export function DataGeneratorPage() {
             <CardHeader>
               <div className="flex items-center justify-between">
                 <div>
-                  <CardTitle>用户用量明细</CardTitle>
+                  <CardTitle>{t("table.userBreakdown.title")}</CardTitle>
                   <CardDescription>
-                    共{" "}
-                    {collapseByUser ? collapsedUserData?.length : userBreakdownResult.items.length}{" "}
-                    条记录
+                    {t("table.userBreakdown.description", {
+                      count: collapseByUser ? collapsedUserData?.length : userBreakdownResult.items.length
+                    })}
                   </CardDescription>
                 </div>
                 <div className="flex items-center space-x-2">
@@ -520,7 +522,7 @@ export function DataGeneratorPage() {
                     onCheckedChange={setCollapseByUser}
                   />
                   <Label htmlFor="collapse-mode" className="cursor-pointer">
-                    按用户折叠
+                    {t("table.userBreakdown.collapseByUser")}
                   </Label>
                 </div>
               </div>
@@ -530,11 +532,11 @@ export function DataGeneratorPage() {
                 <Table>
                   <TableHeader className="sticky top-0 bg-background">
                     <TableRow>
-                      <TableHead>用户名</TableHead>
-                      {!collapseByUser && <TableHead>密钥</TableHead>}
-                      <TableHead>服务模型</TableHead>
-                      <TableHead className="text-right">总调用数</TableHead>
-                      <TableHead className="text-right">总调用额度</TableHead>
+                      <TableHead>{t("table.userBreakdown.columns.userName")}</TableHead>
+                      {!collapseByUser && <TableHead>{t("table.userBreakdown.columns.key")}</TableHead>}
+                      <TableHead>{t("table.userBreakdown.columns.serviceModel")}</TableHead>
+                      <TableHead className="text-right">{t("table.userBreakdown.columns.totalCalls")}</TableHead>
+                      <TableHead className="text-right">{t("table.userBreakdown.columns.totalCost")}</TableHead>
                     </TableRow>
                   </TableHeader>
                   <TableBody>

+ 8 - 1
src/app/[locale]/internal/data-gen/page.tsx

@@ -4,7 +4,14 @@ import { DataGeneratorPage } from "./_components/data-generator-page";
 
 export const dynamic = "force-dynamic";
 
-export default async function Page() {
+export default async function Page({
+  params,
+}: {
+  params: Promise<{ locale: string }>;
+}) {
+  // Await params to ensure locale is available in the async context
+  await params;
+
   const session = await getSession();
 
   if (!session || session.user.role !== "admin") {

+ 6 - 0
src/app/[locale]/login/page.tsx

@@ -10,6 +10,7 @@ import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
 import { Key, Loader2, AlertTriangle, Book } from "lucide-react";
+import { LanguageSwitcher } from "@/components/ui/language-switcher";
 
 export default function LoginPage() {
   return (
@@ -71,6 +72,11 @@ function LoginPageContent() {
 
   return (
     <div className="relative min-h-screen overflow-hidden bg-gradient-to-br from-background via-background to-muted/40">
+      {/* Language Switcher - Fixed Top Right */}
+      <div className="fixed top-4 right-4 z-50">
+        <LanguageSwitcher size="sm" />
+      </div>
+
       <div className="pointer-events-none absolute inset-0 -z-10">
         <div className="absolute right-[10%] top-[-6rem] h-72 w-72 rounded-full bg-orange-500/10 blur-3xl" />
         <div className="absolute bottom-[-4rem] left-[15%] h-80 w-80 rounded-full bg-orange-400/10 blur-3xl" />

+ 7 - 1
src/app/[locale]/page.tsx

@@ -1,5 +1,11 @@
 import { redirect } from "@/i18n/routing";
 
-export default function Home() {
+export default async function Home({
+  params,
+}: {
+  params: Promise<{ locale: string }>;
+}) {
+  // Await params to ensure locale is available in the async context
+  await params;
   redirect("/dashboard" as any);
 }

+ 1 - 2
src/app/[locale]/settings/_components/settings-nav.tsx

@@ -1,7 +1,6 @@
 "use client";
 
-import { Link } from "@/i18n/routing";
-import { usePathname } from "next/navigation";
+import { Link, usePathname } from "@/i18n/routing";
 
 import { cn } from "@/lib/utils";
 import type { SettingsNavItem } from "../_lib/nav-items";

+ 15 - 13
src/app/[locale]/settings/client-versions/_components/client-version-stats-table.tsx

@@ -13,7 +13,7 @@ import {
   TableRow,
 } from "@/components/ui/table";
 import { formatDateDistance } from "@/lib/utils/date-format";
-import { useLocale } from "next-intl";
+import { useLocale, useTranslations } from "next-intl";
 
 interface ClientVersionStatsTableProps {
   data: ClientVersionStats[];
@@ -34,6 +34,8 @@ function getClientTypeIcon(clientType: string): React.ComponentType<{ className?
 
 export function ClientVersionStatsTable({ data }: ClientVersionStatsTableProps) {
   const locale = useLocale();
+  const t = useTranslations("settings.clientVersions.table");
+  const tCommon = useTranslations("settings.common");
 
   return (
     <div className="space-y-8">
@@ -51,14 +53,14 @@ export function ClientVersionStatsTable({ data }: ClientVersionStatsTableProps)
                   {displayName}
                 </h3>
                 <p className="text-sm text-muted-foreground">
-                  内部类型:<code className="text-xs">{clientStats.clientType}</code>
-                  {" · "}当前 GA 版本:
+                  {t("internalType")}<code className="text-xs">{clientStats.clientType}</code>
+                  {" · "}{t("currentGA")}
                   <Badge variant="outline" className="ml-2">
-                    {clientStats.gaVersion || "无(暂无用户使用该版本)"}
+                    {clientStats.gaVersion || tCommon("none")}
                   </Badge>
                 </p>
               </div>
-              <Badge variant="secondary">{clientStats.totalUsers} 位用户</Badge>
+              <Badge variant="secondary">{t("usersCount", { count: clientStats.totalUsers })}</Badge>
             </div>
 
             {/* 用户版本列表 */}
@@ -66,17 +68,17 @@ export function ClientVersionStatsTable({ data }: ClientVersionStatsTableProps)
               <Table>
                 <TableHeader>
                   <TableRow>
-                    <TableHead>用户</TableHead>
-                    <TableHead>当前版本</TableHead>
-                    <TableHead>最后活跃时间</TableHead>
-                    <TableHead>状态</TableHead>
+                    <TableHead>{t("user")}</TableHead>
+                    <TableHead>{t("version")}</TableHead>
+                    <TableHead>{t("lastActive")}</TableHead>
+                    <TableHead>{t("status")}</TableHead>
                   </TableRow>
                 </TableHeader>
                 <TableBody>
                   {clientStats.users.length === 0 ? (
                     <TableRow>
                       <TableCell colSpan={4} className="text-center text-muted-foreground">
-                        暂无用户数据
+                        {t("noUsers")}
                       </TableCell>
                     </TableRow>
                   ) : (
@@ -96,17 +98,17 @@ export function ClientVersionStatsTable({ data }: ClientVersionStatsTableProps)
                               className="bg-green-500 hover:bg-green-600 gap-1"
                             >
                               <Check className="h-3 w-3" />
-                              最新
+                              {t("latest")}
                             </Badge>
                           ) : user.needsUpgrade ? (
                             <Badge variant="destructive" className="gap-1">
                               <AlertTriangle className="h-3 w-3" />
-                              需升级
+                              {t("needsUpgrade")}
                             </Badge>
                           ) : (
                             <Badge variant="outline" className="gap-1">
                               <HelpCircle className="h-3 w-3" />
-                              未知
+                              {t("unknown")}
                             </Badge>
                           )}
                         </TableCell>

+ 15 - 13
src/app/[locale]/settings/client-versions/_components/client-version-toggle.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 import { useState, useTransition } from "react";
+import { useTranslations } from "next-intl";
 import { Label } from "@/components/ui/label";
 import { Switch } from "@/components/ui/switch";
 import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
@@ -13,6 +14,7 @@ interface ClientVersionToggleProps {
 }
 
 export function ClientVersionToggle({ enabled }: ClientVersionToggleProps) {
+  const t = useTranslations("settings.clientVersions");
   const [isEnabled, setIsEnabled] = useState(enabled);
   const [isPending, startTransition] = useTransition();
 
@@ -24,9 +26,9 @@ export function ClientVersionToggle({ enabled }: ClientVersionToggleProps) {
 
       if (result.ok) {
         setIsEnabled(checked);
-        toast.success(checked ? "已启用客户端版本检查" : "已关闭客户端版本检查");
+        toast.success(checked ? t("toggle.enableSuccess") : t("toggle.disableSuccess"));
       } else {
-        toast.error(result.error || "更新失败");
+        toast.error(result.error || t("toggle.toggleFailed"));
       }
     });
   }
@@ -36,8 +38,8 @@ export function ClientVersionToggle({ enabled }: ClientVersionToggleProps) {
       {/* 开关 */}
       <div className="flex items-center justify-between">
         <div className="space-y-1">
-          <Label htmlFor="enable-version-check">启用升级提醒</Label>
-          <p className="text-sm text-muted-foreground">启用后,系统将拦截使用旧版本客户端的请求</p>
+          <Label htmlFor="enable-version-check">{t("toggle.enable")}</Label>
+          <p className="text-sm text-muted-foreground">{t("toggle.description")}</p>
         </div>
         <Switch
           id="enable-version-check"
@@ -50,28 +52,28 @@ export function ClientVersionToggle({ enabled }: ClientVersionToggleProps) {
       {/* 详细说明 */}
       <Alert variant={isEnabled ? "destructive" : "default"}>
         <AlertCircle className="h-4 w-4" />
-        <AlertTitle>功能说明</AlertTitle>
+        <AlertTitle>{t("features.title")}</AlertTitle>
         <AlertDescription className="space-y-3">
           <div>
-            <strong>启用后会发生什么:</strong>
+            <strong>{t("features.whatHappens")}</strong>
           </div>
           <ul className="list-inside list-disc space-y-1">
-            <li>系统会自动检测每种客户端的最新稳定版本(GA 版本)</li>
+            <li>{t("features.autoDetect")}</li>
             <li>
-              <strong>判定规则:</strong>当某个版本被 1 个以上用户使用时,视为 GA 版本
+              <strong>{t("features.gaRule")}</strong>{t("features.gaRuleDesc")}
             </li>
             <li>
-              <strong>活跃窗口:</strong>仅统计过去 7 天内有请求的用户
+              <strong>{t("features.activeWindow")}</strong>{t("features.activeWindowDesc")}
             </li>
             <li className={isEnabled ? "text-destructive font-semibold" : ""}>
-              使用旧版本的用户将收到 HTTP 400 错误,无法继续使用服务
+              {t("features.blockOldVersion")}
             </li>
-            <li>错误提示中包含当前版本和需要升级的版本号</li>
+            <li>{t("features.errorMessage")}</li>
           </ul>
 
           <div className="mt-3 pt-3 border-t">
-            <strong>推荐做法:</strong>
-            <span className="ml-2">先观察下方的版本分布,确认新版本稳定后再启用。</span>
+            <strong>{t("features.recommendation")}</strong>
+            <span className="ml-2">{t("features.recommendationDesc")}</span>
           </div>
         </AlertDescription>
       </Alert>

+ 8 - 1
src/app/[locale]/settings/client-versions/page.tsx

@@ -8,7 +8,14 @@ import { ClientVersionToggle } from "./_components/client-version-toggle";
 import { ClientVersionStatsTable } from "./_components/client-version-stats-table";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 
-export default async function ClientVersionsPage() {
+export default async function ClientVersionsPage({
+  params,
+}: {
+  params: Promise<{ locale: string }>;
+}) {
+  // Await params to ensure locale is available in the async context
+  await params;
+
   const t = await getTranslations("settings");
   const session = await getSession();
 

+ 5 - 3
src/app/[locale]/settings/config/_components/auto-cleanup-form.tsx

@@ -4,6 +4,7 @@ import { useState } from "react";
 import { useForm } from "react-hook-form";
 import { z } from "zod";
 import { zodResolver } from "@hookform/resolvers/zod";
+import { useTranslations } from "next-intl";
 import { Loader2 } from "lucide-react";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
@@ -30,6 +31,7 @@ interface AutoCleanupFormProps {
 }
 
 export function AutoCleanupForm({ settings, onSuccess }: AutoCleanupFormProps) {
+  const t = useTranslations("settings.config.form");
   const [isSubmitting, setIsSubmitting] = useState(false);
 
   const {
@@ -67,14 +69,14 @@ export function AutoCleanupForm({ settings, onSuccess }: AutoCleanupFormProps) {
 
       if (!response.ok) {
         const error = await response.json();
-        throw new Error(error.error || "保存失败");
+        throw new Error(error.error || t("saveFailed"));
       }
 
-      toast.success("自动清理配置已保存");
+      toast.success(t("autoCleanupSaved"));
       onSuccess?.();
     } catch (error) {
       console.error("Save error:", error);
-      toast.error(error instanceof Error ? error.message : "保存配置失败");
+      toast.error(error instanceof Error ? error.message : t("saveError"));
     } finally {
       setIsSubmitting(false);
     }

+ 3 - 0
src/app/[locale]/settings/layout.tsx

@@ -13,6 +13,9 @@ export default async function SettingsLayout({
   children: ReactNode;
   params: Promise<{ locale: string }>;
 }) {
+  // Await params to ensure locale is available in the async context
+  await params;
+
   const session = await getSession();
 
   if (!session) {

+ 7 - 1
src/app/[locale]/settings/page.tsx

@@ -2,7 +2,13 @@ import { redirect } from "@/i18n/routing";
 
 import { SETTINGS_NAV_ITEMS } from "./_lib/nav-items";
 
-export default function SettingsIndex() {
+export default async function SettingsIndex({
+  params,
+}: {
+  params: Promise<{ locale: string }>;
+}) {
+  // Await params to ensure locale is available in the async context
+  await params;
   const firstItem = SETTINGS_NAV_ITEMS[0];
   const href = firstItem?.href ?? "/dashboard";
   redirect(href as any);

+ 14 - 8
src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 import { useState } from "react";
+import { useTranslations } from "next-intl";
 import { RefreshCw } from "lucide-react";
 import { Button } from "@/components/ui/button";
 import { syncLiteLLMPrices } from "@/actions/model-prices";
@@ -11,6 +12,7 @@ import { useRouter } from "next/navigation";
  * LiteLLM 价格同步按钮组件
  */
 export function SyncLiteLLMButton() {
+  const t = useTranslations("settings");
   const router = useRouter();
   const [syncing, setSyncing] = useState(false);
 
@@ -21,12 +23,12 @@ export function SyncLiteLLMButton() {
       const response = await syncLiteLLMPrices();
 
       if (!response.ok) {
-        toast.error(response.error || "同步失败");
+        toast.error(response.error || t("prices.sync.failed"));
         return;
       }
 
       if (!response.data) {
-        toast.error("同步成功但未返回处理结果");
+        toast.error(t("prices.sync.failedNoResult"));
         return;
       }
 
@@ -35,23 +37,27 @@ export function SyncLiteLLMButton() {
       // 显示详细结果
       if (added.length > 0 || updated.length > 0) {
         toast.success(
-          `同步成功:新增 ${added.length} 个,更新 ${updated.length} 个,未变化 ${unchanged.length} 个`
+          t("prices.sync.successWithChanges", {
+            added: added.length,
+            updated: updated.length,
+            unchanged: unchanged.length,
+          })
         );
       } else if (unchanged.length > 0) {
-        toast.info(`所有 ${unchanged.length} 个模型价格均为最新`);
+        toast.info(t("prices.sync.successNoChanges", { unchanged: unchanged.length }));
       } else {
-        toast.warning("未找到支持的模型价格");
+        toast.warning(t("prices.sync.noModels"));
       }
 
       if (failed.length > 0) {
-        toast.error(`${failed.length} 个模型处理失败`);
+        toast.error(t("prices.sync.partialFailure", { failed: failed.length }));
       }
 
       // 刷新页面数据
       router.refresh();
     } catch (error) {
       console.error("同步失败:", error);
-      toast.error("同步失败,请重试");
+      toast.error(t("prices.sync.failedError"));
     } finally {
       setSyncing(false);
     }
@@ -60,7 +66,7 @@ export function SyncLiteLLMButton() {
   return (
     <Button variant="outline" size="sm" onClick={handleSync} disabled={syncing}>
       <RefreshCw className={`h-4 w-4 mr-2 ${syncing ? "animate-spin" : ""}`} />
-      {syncing ? "同步中..." : "同步 LiteLLM 价格"}
+      {syncing ? t("prices.sync.syncing") : t("prices.sync.button")}
     </Button>
   );
 }

+ 20 - 18
src/app/[locale]/settings/sensitive-words/_components/add-word-dialog.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 import { useState } from "react";
+import { useTranslations } from "next-intl";
 import { Button } from "@/components/ui/button";
 import {
   Dialog,
@@ -26,6 +27,7 @@ import { createSensitiveWordAction } from "@/actions/sensitive-words";
 import { toast } from "sonner";
 
 export function AddWordDialog() {
+  const t = useTranslations("settings");
   const [open, setOpen] = useState(false);
   const [isSubmitting, setIsSubmitting] = useState(false);
   const [word, setWord] = useState("");
@@ -36,7 +38,7 @@ export function AddWordDialog() {
     e.preventDefault();
 
     if (!word.trim()) {
-      toast.error("请输入敏感词");
+      toast.error(t("sensitiveWords.dialog.wordRequired"));
       return;
     }
 
@@ -50,7 +52,7 @@ export function AddWordDialog() {
       });
 
       if (result.ok) {
-        toast.success("敏感词创建成功");
+        toast.success(t("sensitiveWords.addSuccess"));
         setOpen(false);
         // 重置表单
         setWord("");
@@ -60,7 +62,7 @@ export function AddWordDialog() {
         toast.error(result.error);
       }
     } catch {
-      toast.error("创建敏感词失败");
+      toast.error(t("sensitiveWords.addFailed"));
     } finally {
       setIsSubmitting(false);
     }
@@ -71,32 +73,30 @@ export function AddWordDialog() {
       <DialogTrigger asChild>
         <Button>
           <Plus className="mr-2 h-4 w-4" />
-          添加敏感词
+          {t("sensitiveWords.add")}
         </Button>
       </DialogTrigger>
       <DialogContent>
         <form onSubmit={handleSubmit}>
           <DialogHeader>
-            <DialogTitle>添加敏感词</DialogTitle>
-            <DialogDescription>
-              配置敏感词过滤规则,被命中的请求将不会转发到上游。
-            </DialogDescription>
+            <DialogTitle>{t("sensitiveWords.dialog.addTitle")}</DialogTitle>
+            <DialogDescription>{t("sensitiveWords.dialog.addDescription")}</DialogDescription>
           </DialogHeader>
 
           <div className="grid gap-4 py-4">
             <div className="grid gap-2">
-              <Label htmlFor="word">敏感词 *</Label>
+              <Label htmlFor="word">{t("sensitiveWords.dialog.wordLabel")}</Label>
               <Input
                 id="word"
                 value={word}
                 onChange={(e) => setWord(e.target.value)}
-                placeholder="输入敏感词..."
+                placeholder={t("sensitiveWords.dialog.wordPlaceholder")}
                 required
               />
             </div>
 
             <div className="grid gap-2">
-              <Label htmlFor="matchType">匹配类型 *</Label>
+              <Label htmlFor="matchType">{t("sensitiveWords.dialog.matchTypeLabel")}</Label>
               <Select
                 value={matchType}
                 onValueChange={(value) => setMatchType(value as "contains" | "exact" | "regex")}
@@ -105,20 +105,22 @@ export function AddWordDialog() {
                   <SelectValue />
                 </SelectTrigger>
                 <SelectContent>
-                  <SelectItem value="contains">包含匹配 - 文本中包含该词即拦截</SelectItem>
-                  <SelectItem value="exact">精确匹配 - 完全匹配该词才拦截</SelectItem>
-                  <SelectItem value="regex">正则表达式 - 支持复杂模式匹配</SelectItem>
+                  <SelectItem value="contains">
+                    {t("sensitiveWords.dialog.matchTypeContains")}
+                  </SelectItem>
+                  <SelectItem value="exact">{t("sensitiveWords.dialog.matchTypeExact")}</SelectItem>
+                  <SelectItem value="regex">{t("sensitiveWords.dialog.matchTypeRegex")}</SelectItem>
                 </SelectContent>
               </Select>
             </div>
 
             <div className="grid gap-2">
-              <Label htmlFor="description">说明</Label>
+              <Label htmlFor="description">{t("sensitiveWords.dialog.descriptionLabel")}</Label>
               <Textarea
                 id="description"
                 value={description}
                 onChange={(e) => setDescription(e.target.value)}
-                placeholder="可选:添加说明..."
+                placeholder={t("sensitiveWords.dialog.descriptionPlaceholder")}
                 rows={3}
               />
             </div>
@@ -131,10 +133,10 @@ export function AddWordDialog() {
               onClick={() => setOpen(false)}
               disabled={isSubmitting}
             >
-              取消
+              {t("common.cancel")}
             </Button>
             <Button type="submit" disabled={isSubmitting}>
-              {isSubmitting ? "创建中..." : "创建"}
+              {isSubmitting ? t("sensitiveWords.dialog.creating") : t("common.create")}
             </Button>
           </DialogFooter>
         </form>

+ 19 - 15
src/app/[locale]/settings/sensitive-words/_components/edit-word-dialog.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 import { useState, useEffect } from "react";
+import { useTranslations } from "next-intl";
 import { Button } from "@/components/ui/button";
 import {
   Dialog,
@@ -31,6 +32,7 @@ interface EditWordDialogProps {
 }
 
 export function EditWordDialog({ word, open, onOpenChange }: EditWordDialogProps) {
+  const t = useTranslations("settings");
   const [isSubmitting, setIsSubmitting] = useState(false);
   const [wordText, setWordText] = useState("");
   const [matchType, setMatchType] = useState<string>("");
@@ -49,7 +51,7 @@ export function EditWordDialog({ word, open, onOpenChange }: EditWordDialogProps
     e.preventDefault();
 
     if (!wordText.trim()) {
-      toast.error("请输入敏感词");
+      toast.error(t("sensitiveWords.dialog.wordRequired"));
       return;
     }
 
@@ -63,13 +65,13 @@ export function EditWordDialog({ word, open, onOpenChange }: EditWordDialogProps
       });
 
       if (result.ok) {
-        toast.success("敏感词更新成功");
+        toast.success(t("sensitiveWords.editSuccess"));
         onOpenChange(false);
       } else {
         toast.error(result.error);
       }
     } catch {
-      toast.error("更新敏感词失败");
+      toast.error(t("sensitiveWords.editFailed"));
     } finally {
       setIsSubmitting(false);
     }
@@ -80,43 +82,45 @@ export function EditWordDialog({ word, open, onOpenChange }: EditWordDialogProps
       <DialogContent>
         <form onSubmit={handleSubmit}>
           <DialogHeader>
-            <DialogTitle>编辑敏感词</DialogTitle>
-            <DialogDescription>修改敏感词配置,更改后将自动刷新缓存。</DialogDescription>
+            <DialogTitle>{t("sensitiveWords.dialog.editTitle")}</DialogTitle>
+            <DialogDescription>{t("sensitiveWords.dialog.editDescription")}</DialogDescription>
           </DialogHeader>
 
           <div className="grid gap-4 py-4">
             <div className="grid gap-2">
-              <Label htmlFor="edit-word">敏感词 *</Label>
+              <Label htmlFor="edit-word">{t("sensitiveWords.dialog.wordLabel")}</Label>
               <Input
                 id="edit-word"
                 value={wordText}
                 onChange={(e) => setWordText(e.target.value)}
-                placeholder="输入敏感词..."
+                placeholder={t("sensitiveWords.dialog.wordPlaceholder")}
                 required
               />
             </div>
 
             <div className="grid gap-2">
-              <Label htmlFor="edit-matchType">匹配类型 *</Label>
+              <Label htmlFor="edit-matchType">{t("sensitiveWords.dialog.matchTypeLabel")}</Label>
               <Select value={matchType} onValueChange={(value) => setMatchType(value)}>
                 <SelectTrigger>
                   <SelectValue />
                 </SelectTrigger>
                 <SelectContent>
-                  <SelectItem value="contains">包含匹配 - 文本中包含该词即拦截</SelectItem>
-                  <SelectItem value="exact">精确匹配 - 完全匹配该词才拦截</SelectItem>
-                  <SelectItem value="regex">正则表达式 - 支持复杂模式匹配</SelectItem>
+                  <SelectItem value="contains">
+                    {t("sensitiveWords.dialog.matchTypeContains")}
+                  </SelectItem>
+                  <SelectItem value="exact">{t("sensitiveWords.dialog.matchTypeExact")}</SelectItem>
+                  <SelectItem value="regex">{t("sensitiveWords.dialog.matchTypeRegex")}</SelectItem>
                 </SelectContent>
               </Select>
             </div>
 
             <div className="grid gap-2">
-              <Label htmlFor="edit-description">说明</Label>
+              <Label htmlFor="edit-description">{t("sensitiveWords.dialog.descriptionLabel")}</Label>
               <Textarea
                 id="edit-description"
                 value={description}
                 onChange={(e) => setDescription(e.target.value)}
-                placeholder="可选:添加说明..."
+                placeholder={t("sensitiveWords.dialog.descriptionPlaceholder")}
                 rows={3}
               />
             </div>
@@ -129,10 +133,10 @@ export function EditWordDialog({ word, open, onOpenChange }: EditWordDialogProps
               onClick={() => onOpenChange(false)}
               disabled={isSubmitting}
             >
-              取消
+              {t("common.cancel")}
             </Button>
             <Button type="submit" disabled={isSubmitting}>
-              {isSubmitting ? "保存中..." : "保存"}
+              {isSubmitting ? t("sensitiveWords.dialog.saving") : t("common.save")}
             </Button>
           </DialogFooter>
         </form>

+ 11 - 5
src/app/[locale]/settings/sensitive-words/_components/refresh-cache-button.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 import { useState } from "react";
+import { useTranslations } from "next-intl";
 import { Button } from "@/components/ui/button";
 import { RefreshCw } from "lucide-react";
 import { refreshCacheAction } from "@/actions/sensitive-words";
@@ -17,6 +18,7 @@ interface RefreshCacheButtonProps {
 }
 
 export function RefreshCacheButton({ stats }: RefreshCacheButtonProps) {
+  const t = useTranslations("settings");
   const [isRefreshing, setIsRefreshing] = useState(false);
 
   const handleRefresh = async () => {
@@ -27,12 +29,12 @@ export function RefreshCacheButton({ stats }: RefreshCacheButtonProps) {
 
       if (result.ok) {
         const count = result.data.stats.totalCount;
-        toast.success(`缓存刷新成功,已加载 ${count} 个敏感词`);
+        toast.success(t("sensitiveWords.refreshCacheSuccess", { count }));
       } else {
         toast.error(result.error);
       }
     } catch {
-      toast.error("刷新缓存失败");
+      toast.error(t("sensitiveWords.refreshCacheFailed"));
     } finally {
       setIsRefreshing(false);
     }
@@ -45,12 +47,16 @@ export function RefreshCacheButton({ stats }: RefreshCacheButtonProps) {
       disabled={isRefreshing}
       title={
         stats
-          ? `缓存统计:包含(${stats.containsCount}) 精确(${stats.exactCount}) 正则(${stats.regexCount})`
-          : "刷新缓存"
+          ? t("sensitiveWords.cacheStats", {
+              containsCount: stats.containsCount,
+              exactCount: stats.exactCount,
+              regexCount: stats.regexCount,
+            })
+          : t("sensitiveWords.refreshCache")
       }
     >
       <RefreshCw className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
-      刷新缓存
+      {t("sensitiveWords.refreshCache")}
       {stats && <span className="ml-2 text-xs text-muted-foreground">({stats.totalCount})</span>}
     </Button>
   );

+ 30 - 16
src/app/[locale]/settings/sensitive-words/_components/word-list-table.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 import { useState } from "react";
+import { useTranslations } from "next-intl";
 import type { SensitiveWord } from "@/repository/sensitive-words";
 import { updateSensitiveWordAction, deleteSensitiveWordAction } from "@/actions/sensitive-words";
 import { Button } from "@/components/ui/button";
@@ -14,12 +15,6 @@ interface WordListTableProps {
   words: SensitiveWord[];
 }
 
-const matchTypeLabels = {
-  contains: "包含匹配",
-  exact: "精确匹配",
-  regex: "正则表达式",
-};
-
 const matchTypeColors = {
   contains: "default" as const,
   exact: "secondary" as const,
@@ -27,28 +22,35 @@ const matchTypeColors = {
 };
 
 export function WordListTable({ words }: WordListTableProps) {
+  const t = useTranslations("settings");
   const [selectedWord, setSelectedWord] = useState<SensitiveWord | null>(null);
   const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
 
+  const matchTypeLabels = {
+    contains: t("sensitiveWords.table.matchTypeContains"),
+    exact: t("sensitiveWords.table.matchTypeExact"),
+    regex: t("sensitiveWords.table.matchTypeRegex"),
+  };
+
   const handleToggleEnabled = async (id: number, isEnabled: boolean) => {
     const result = await updateSensitiveWordAction(id, { isEnabled });
 
     if (result.ok) {
-      toast.success(isEnabled ? "敏感词已启用" : "敏感词已禁用");
+      toast.success(isEnabled ? t("sensitiveWords.enable") : t("sensitiveWords.disable"));
     } else {
       toast.error(result.error);
     }
   };
 
   const handleDelete = async (id: number, word: string) => {
-    if (!confirm(`确定要删除敏感词"${word}"吗?`)) {
+    if (!confirm(t("sensitiveWords.confirmDelete", { word }))) {
       return;
     }
 
     const result = await deleteSensitiveWordAction(id);
 
     if (result.ok) {
-      toast.success("敏感词删除成功");
+      toast.success(t("sensitiveWords.deleteSuccess"));
     } else {
       toast.error(result.error);
     }
@@ -62,7 +64,7 @@ export function WordListTable({ words }: WordListTableProps) {
   if (words.length === 0) {
     return (
       <div className="flex h-32 items-center justify-center text-sm text-muted-foreground">
-        暂无敏感词,点击右上角&ldquo;添加敏感词&rdquo;开始配置。
+        {t("sensitiveWords.emptyState")}
       </div>
     );
   }
@@ -73,12 +75,24 @@ export function WordListTable({ words }: WordListTableProps) {
         <table className="w-full border-collapse">
           <thead>
             <tr className="border-b bg-muted/50">
-              <th className="px-4 py-3 text-left text-sm font-medium">敏感词</th>
-              <th className="px-4 py-3 text-left text-sm font-medium">匹配类型</th>
-              <th className="px-4 py-3 text-left text-sm font-medium">说明</th>
-              <th className="px-4 py-3 text-left text-sm font-medium">状态</th>
-              <th className="px-4 py-3 text-left text-sm font-medium">创建时间</th>
-              <th className="px-4 py-3 text-right text-sm font-medium">操作</th>
+              <th className="px-4 py-3 text-left text-sm font-medium">
+                {t("sensitiveWords.table.word")}
+              </th>
+              <th className="px-4 py-3 text-left text-sm font-medium">
+                {t("sensitiveWords.table.matchType")}
+              </th>
+              <th className="px-4 py-3 text-left text-sm font-medium">
+                {t("sensitiveWords.table.description")}
+              </th>
+              <th className="px-4 py-3 text-left text-sm font-medium">
+                {t("sensitiveWords.table.status")}
+              </th>
+              <th className="px-4 py-3 text-left text-sm font-medium">
+                {t("sensitiveWords.table.createdAt")}
+              </th>
+              <th className="px-4 py-3 text-right text-sm font-medium">
+                {t("sensitiveWords.table.actions")}
+              </th>
             </tr>
           </thead>
           <tbody>

Разница между файлами не показана из-за своего большого размера
+ 283 - 334
src/app/[locale]/usage-doc/page.tsx


+ 2 - 1
src/components/ui/language-switcher.tsx

@@ -58,7 +58,8 @@ export function LanguageSwitcher({ className, size = "sm" }: LanguageSwitcherPro
         // 2. Sets the NEXT_LOCALE cookie
         // 3. Maintains the current pathname
         // 4. Preserves query parameters and hash
-        router.push(pathname, { locale: newLocale as Locale });
+        // Fallback to dashboard if pathname is undefined
+        router.push(pathname || "/dashboard", { locale: newLocale as Locale });
       } catch (error) {
         console.error("Failed to switch locale:", error);
         setIsTransitioning(false);

Некоторые файлы не были показаны из-за большого количества измененных файлов