Commit 524455fa by CaIon

feat(redemptions): add batch deletion and optional file exports

Add confirmed multi-select deletion through one batch API request and one
soft-delete statement. Record the affected count and requested IDs in a
separate batch audit event, and identify legacy events with missing counts.

After creation, offer an unchecked Save as a file option with TXT/Markdown
formats and optional name/quota fields. Keep Done as the default completion
action. Include translations for all seven frontend locales.

Validation:
- Redemption and audit frontend regression tests, typecheck, and scoped lint.
- go build ./...
- go test ./controller -run '^TestDeleteRedemptionBatch$' -count=1 -v
  with TEST_MYSQL_DSN, TEST_MYSQL_LOG_DSN, TEST_POSTGRES_DSN,
  and TEST_POSTGRES_LOG_DSN set to isolated primary and log databases.
- Real SQLite 3.50.4, MySQL 8.4.11, and PostgreSQL 16.15 passed, including
  deletion of 15 records, duplicate/missing IDs, zero-row retries, audit
  deduplication, invalid input, and exclusion of credentials from logs.
parent 950644c9
...@@ -59,6 +59,7 @@ var auditContentTemplates = map[string]string{ ...@@ -59,6 +59,7 @@ var auditContentTemplates = map[string]string{
"channel.upstream_apply_all": "Applied upstream model changes to ${count} channels", "channel.upstream_apply_all": "Applied upstream model changes to ${count} channels",
"redemption.create": "Created ${count} redemption codes named ${name} (${quota} each)", "redemption.create": "Created ${count} redemption codes named ${name} (${quota} each)",
"redemption.delete_batch": "Batch deleted ${count} redemption codes",
"subscription.plan_reset": "Reset active subscriptions for plan ${plan_id}", "subscription.plan_reset": "Reset active subscriptions for plan ${plan_id}",
"subscription.user_plan_reset": "Reset active plan ${plan_id} subscriptions for user ${target_user_id}", "subscription.user_plan_reset": "Reset active plan ${plan_id} subscriptions for user ${target_user_id}",
......
...@@ -215,3 +215,24 @@ func validateExpiredTime(c *gin.Context, expired int64) (bool, string) { ...@@ -215,3 +215,24 @@ func validateExpiredTime(c *gin.Context, expired int64) (bool, string) {
} }
return true, "" return true, ""
} }
func DeleteRedemptionBatch(c *gin.Context) {
var request struct {
Ids []int `json:"ids" binding:"required,min=1,max=1000,dive,gt=0"`
}
if err := c.ShouldBindJSON(&request); err != nil {
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
count, err := model.BatchDeleteRedemptions(request.Ids)
if err != nil {
common.ApiError(c, err)
return
}
recordManageAudit(c, "redemption.delete_batch", map[string]any{
"count": count,
"total": len(request.Ids),
"requested_redemption_ids": request.Ids,
})
common.ApiSuccess(c, count)
}
package controller
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/middleware"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func TestDeleteRedemptionBatch(t *testing.T) {
for _, dialect := range []string{"sqlite", "mysql", "postgres"} {
t.Run(dialect, func(t *testing.T) {
var driver, logDriver gorm.Dialector
dbType := common.DatabaseTypeSQLite
switch dialect {
case "sqlite":
driver = sqlite.Open(":memory:")
logDriver = sqlite.Open(":memory:")
case "mysql":
dsn := os.Getenv("TEST_MYSQL_DSN")
if dsn == "" {
t.Skip("TEST_MYSQL_DSN is not configured")
}
driver = mysql.Open(dsn)
logDSN := os.Getenv("TEST_MYSQL_LOG_DSN")
if logDSN == "" {
logDSN = dsn
}
logDriver = mysql.Open(logDSN)
dbType = common.DatabaseTypeMySQL
case "postgres":
dsn := os.Getenv("TEST_POSTGRES_DSN")
if dsn == "" {
t.Skip("TEST_POSTGRES_DSN is not configured")
}
driver = postgres.Open(dsn)
logDSN := os.Getenv("TEST_POSTGRES_LOG_DSN")
if logDSN == "" {
logDSN = dsn
}
logDriver = postgres.Open(logDSN)
dbType = common.DatabaseTypePostgreSQL
}
db, err := gorm.Open(driver, &gorm.Config{})
require.NoError(t, err)
sqlDB, err := db.DB()
require.NoError(t, err)
sqlDB.SetMaxOpenConns(1)
t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
var version string
query := "SELECT version()"
if dialect == "sqlite" {
query = "SELECT sqlite_version()"
}
require.NoError(t, db.Raw(query).Scan(&version).Error)
t.Logf("database version: %s", version)
logDB, err := gorm.Open(logDriver, &gorm.Config{})
require.NoError(t, err)
logSQL, err := logDB.DB()
require.NoError(t, err)
logSQL.SetMaxOpenConns(1)
t.Cleanup(func() { require.NoError(t, logSQL.Close()) })
previousDB, previousLogDB := model.DB, model.LOG_DB
previousMain, previousLog := common.MainDatabaseType(), common.LogDatabaseType()
previousRedis := common.RedisEnabled
model.DB, model.LOG_DB = db, logDB
common.SetDatabaseTypes(dbType, dbType)
common.RedisEnabled = false
t.Cleanup(func() {
model.DB, model.LOG_DB = previousDB, previousLogDB
common.SetDatabaseTypes(previousMain, previousLog)
common.RedisEnabled = previousRedis
})
for _, table := range []any{&model.User{}, &model.Redemption{}} {
require.False(t, db.Migrator().HasTable(table), "use an empty test database")
require.NoError(t, db.AutoMigrate(table))
t.Cleanup(func() { require.NoError(t, db.Migrator().DropTable(table)) })
}
require.False(t, logDB.Migrator().HasTable(&model.AuditLog{}), "use an empty test log database")
require.NoError(t, logDB.AutoMigrate(&model.AuditLog{}))
t.Cleanup(func() { require.NoError(t, logDB.Migrator().DropTable(&model.AuditLog{})) })
token := "redemption-audit-test-token"
admin := model.User{Username: "redemption-audit-admin", Password: "unused", Role: common.RoleAdminUser, Status: common.UserStatusEnabled, Group: "default", AccessToken: &token}
require.NoError(t, db.Create(&admin).Error)
codes := make([]model.Redemption, 16)
for index := range codes {
codes[index] = model.Redemption{Name: "selected", Key: fmt.Sprintf("%032d", index+1), Quota: 100, Status: common.RedemptionCodeStatusEnabled}
}
codes[1].Status = common.RedemptionCodeStatusUsed
codes[15].Name = "unselected"
codes[15].Status = common.RedemptionCodeStatusDisabled
require.NoError(t, model.DB.Create(&codes).Error)
router := gin.New()
router.Use(middleware.RequestId())
router.POST("/api/redemption/batch", middleware.AdminAuth(), DeleteRedemptionBatch)
overLimit := make([]int, 1001)
for index := range overLimit {
overLimit[index] = codes[0].Id
}
oversized, err := common.Marshal(map[string]any{"ids": overLimit})
require.NoError(t, err)
for _, body := range []string{"{}", `{"ids":[]}`, `{"ids":null}`, `{"ids":[0]}`, `{"ids":[1,-1]}`, `{"ids":["1"]}`, "{", string(oversized)} {
t.Run("invalid_"+body[:min(len(body), 30)], func(t *testing.T) {
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/api/redemption/batch", bytes.NewBufferString(body))
request.Header.Set("Authorization", "Bearer "+token)
router.ServeHTTP(response, request)
var result struct {
Success bool `json:"success"`
}
require.NoError(t, common.Unmarshal(response.Body.Bytes(), &result))
assert.False(t, result.Success)
var count int64
require.NoError(t, model.DB.Model(&model.Redemption{}).Count(&count).Error)
assert.EqualValues(t, 16, count)
var events []model.AuditLog
require.NoError(t, logDB.Where("request_id = ? AND category = ?", response.Header().Get(common.RequestIdKey), model.AuditCategoryOperation).Find(&events).Error)
require.Len(t, events, 1)
assert.False(t, events[0].Success)
assert.Equal(t, "redemption.delete_batch", events[0].Action)
})
}
_, err = model.BatchDeleteRedemptions(nil)
require.Error(t, err)
requestedIDs := make([]int, 0, 17)
for _, code := range codes[:15] {
requestedIDs = append(requestedIDs, code.Id)
}
requestedIDs = append(requestedIDs, codes[0].Id, 999999)
payload, err := common.Marshal(map[string]any{"ids": requestedIDs})
require.NoError(t, err)
for _, expectedCount := range []int64{15, 0} {
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/api/redemption/batch", bytes.NewReader(payload))
request.Header.Set("Authorization", "Bearer "+token)
router.ServeHTTP(response, request)
assert.Equal(t, http.StatusOK, response.Code)
var result struct {
Success bool `json:"success"`
Data int64 `json:"data"`
}
require.NoError(t, common.Unmarshal(response.Body.Bytes(), &result))
assert.True(t, result.Success)
assert.Equal(t, expectedCount, result.Data)
var events []model.AuditLog
require.NoError(t, logDB.Where("request_id = ? AND category = ?", response.Header().Get(common.RequestIdKey), model.AuditCategoryOperation).Find(&events).Error)
require.Len(t, events, 1, "one operation event, without a duplicate single-delete fallback")
event := events[0]
assert.Equal(t, "redemption.delete_batch", event.Action)
assert.Equal(t, fmt.Sprintf("Batch deleted %d redemption codes", expectedCount), event.Content)
assert.True(t, event.Success)
assert.Equal(t, admin.Id, event.UserId)
assert.Equal(t, "/api/redemption/batch", event.Route)
require.NotNil(t, event.Other.Op)
encoded, err := common.Marshal(event.Other.Op.Params)
require.NoError(t, err)
var params struct {
Count int64 `json:"count"`
Total int `json:"total"`
IDs []int `json:"requested_redemption_ids"`
}
require.NoError(t, common.Unmarshal(encoded, &params))
assert.Equal(t, expectedCount, params.Count)
assert.Equal(t, len(requestedIDs), params.Total)
assert.Equal(t, requestedIDs, params.IDs)
encoded, err = common.Marshal(event)
require.NoError(t, err)
assert.NotContains(t, string(encoded), token)
for _, code := range codes {
assert.NotContains(t, string(encoded), code.Key)
}
}
var active []model.Redemption
require.NoError(t, model.DB.Find(&active).Error)
require.Len(t, active, 1)
assert.Equal(t, codes[15], active[0])
var all []model.Redemption
require.NoError(t, model.DB.Unscoped().Order("id").Find(&all).Error)
require.Len(t, all, 16)
for _, code := range all[:15] {
assert.True(t, code.DeletedAt.Valid)
}
assert.False(t, all[15].DeletedAt.Valid)
})
}
}
...@@ -64,6 +64,7 @@ var auditRouteActions = map[string]string{ ...@@ -64,6 +64,7 @@ var auditRouteActions = map[string]string{
// 兑换码 // 兑换码
"PUT /api/redemption/": "redemption.update", "PUT /api/redemption/": "redemption.update",
"POST /api/redemption/batch": "redemption.delete_batch",
"DELETE /api/redemption/:id": "redemption.delete", "DELETE /api/redemption/:id": "redemption.delete",
"DELETE /api/redemption/invalid": "redemption.delete_invalid", "DELETE /api/redemption/invalid": "redemption.delete_invalid",
......
...@@ -239,3 +239,17 @@ func DeleteInvalidRedemptions() (int64, error) { ...@@ -239,3 +239,17 @@ func DeleteInvalidRedemptions() (int64, error) {
result := DB.Where("status IN ? OR (status = ? AND expired_time != 0 AND expired_time < ?)", []int{common.RedemptionCodeStatusUsed, common.RedemptionCodeStatusDisabled}, common.RedemptionCodeStatusEnabled, now).Delete(&Redemption{}) result := DB.Where("status IN ? OR (status = ? AND expired_time != 0 AND expired_time < ?)", []int{common.RedemptionCodeStatusUsed, common.RedemptionCodeStatusDisabled}, common.RedemptionCodeStatusEnabled, now).Delete(&Redemption{})
return result.RowsAffected, result.Error return result.RowsAffected, result.Error
} }
// BatchDeleteRedemptions soft-deletes the selected codes in one statement.
func BatchDeleteRedemptions(ids []int) (int64, error) {
if len(ids) == 0 || len(ids) > 1000 {
return 0, errors.New("select between 1 and 1000 redemption codes")
}
for _, id := range ids {
if id <= 0 {
return 0, errors.New("redemption IDs must be positive")
}
}
result := DB.Where("id IN ?", ids).Delete(&Redemption{})
return result.RowsAffected, result.Error
}
...@@ -298,6 +298,7 @@ func SetApiRouter(router *gin.Engine) { ...@@ -298,6 +298,7 @@ func SetApiRouter(router *gin.Engine) {
redemptionRoute.GET("/search", controller.SearchRedemptions) redemptionRoute.GET("/search", controller.SearchRedemptions)
redemptionRoute.GET("/:id", controller.GetRedemption) redemptionRoute.GET("/:id", controller.GetRedemption)
redemptionRoute.POST("/", controller.AddRedemption) redemptionRoute.POST("/", controller.AddRedemption)
redemptionRoute.POST("/batch", controller.DeleteRedemptionBatch)
redemptionRoute.PUT("/", controller.UpdateRedemption) redemptionRoute.PUT("/", controller.UpdateRedemption)
redemptionRoute.DELETE("/invalid", controller.DeleteInvalidRedemption) redemptionRoute.DELETE("/invalid", controller.DeleteInvalidRedemption)
redemptionRoute.DELETE("/:id", controller.DeleteRedemption) redemptionRoute.DELETE("/:id", controller.DeleteRedemption)
......
...@@ -98,3 +98,10 @@ export async function deleteInvalidRedemptions(): Promise<ApiResponse<number>> { ...@@ -98,3 +98,10 @@ export async function deleteInvalidRedemptions(): Promise<ApiResponse<number>> {
const res = await api.delete('/api/redemption/invalid') const res = await api.delete('/api/redemption/invalid')
return res.data return res.data
} }
export async function batchDeleteRedemptions(
ids: number[]
): Promise<ApiResponse<number>> {
const res = await api.post('/api/redemption/batch', { ids })
return res.data
}
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/react-query'
import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Toaster, toast } from 'sonner'
import { afterEach, expect, test, vi } from 'vitest'
import { useDataTable } from '@/components/data-table'
import { api } from '@/lib/api'
import { getRedemptions } from '../../api'
import type { Redemption } from '../../types'
import { DataTableBulkActions } from '../data-table-bulk-actions'
import { RedemptionsProvider, useRedemptions } from '../redemptions-provider'
const codes: Redemption[] = [11, 22, 33].map((id) => ({
id,
user_id: 1,
name: `code-${id}`,
key: `key-${id}`,
status: 1,
quota: 100,
created_time: 1,
redeemed_time: 0,
expired_time: 0,
used_user_id: 0,
}))
const columns = [{ accessorKey: 'name' }]
const clients: QueryClient[] = []
function RedemptionList() {
const { refreshTrigger } = useRedemptions()
const { data } = useQuery({
queryKey: ['redemptions', refreshTrigger],
queryFn: () => getRedemptions(),
})
const { table } = useDataTable({
data: data?.data?.items ?? [],
columns,
enableRowSelection: true,
getRowId: (row) => String(row.id),
})
return (
<>
{table.getRowModel().rows.map((row) => (
<label key={row.id}>
<input
type='checkbox'
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
{row.original.name}
</label>
))}
<DataTableBulkActions table={table} />
</>
)
}
async function setup() {
let remaining = [...codes]
vi.spyOn(api, 'get').mockImplementation(async () => ({
data: {
success: true,
data: { items: remaining, total: remaining.length },
},
}))
const remove = vi
.spyOn(api, 'post')
.mockImplementation(async (_url, body) => {
const { ids } = body as { ids: number[] }
const count = remaining.filter((code) => ids.includes(code.id)).length
remaining = remaining.filter((code) => !ids.includes(code.id))
return { data: { success: true, data: count } }
})
const client = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
clients.push(client)
render(
<QueryClientProvider client={client}>
<RedemptionsProvider>
<RedemptionList />
</RedemptionsProvider>
<Toaster />
</QueryClientProvider>
)
await screen.findByRole('checkbox', { name: 'code-11' })
return { user: userEvent.setup(), remove }
}
afterEach(() => {
toast.dismiss()
for (const client of clients) client.clear()
clients.length = 0
})
test('shows deletion only for selected codes and cancellation sends no requests', async () => {
const { user, remove } = await setup()
expect(
screen.queryByRole('button', { name: 'Delete selected redemption codes' })
).not.toBeInTheDocument()
await user.click(screen.getByRole('checkbox', { name: 'code-11' }))
const button = screen.getByRole('button', {
name: 'Delete selected redemption codes',
})
button.focus()
await user.keyboard('{Enter}')
const dialog = await screen.findByRole('alertdialog')
expect(dialog).toHaveAccessibleName('Delete 1 redemption codes?')
await user.click(within(dialog).getByRole('button', { name: 'Cancel' }))
await waitFor(() =>
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
)
expect(remove).not.toHaveBeenCalled()
expect(screen.getByRole('checkbox', { name: 'code-11' })).toBeChecked()
})
test('confirmation deletes only selected codes, disables repeat submissions and refreshes the list', async () => {
const { user, remove } = await setup()
let release!: () => void
const pending = new Promise<void>((resolve) => {
release = resolve
})
const originalDelete = remove.getMockImplementation()
if (!originalDelete) throw new Error('Missing batch delete mock')
remove.mockImplementationOnce(async (url, body) => {
await pending
return originalDelete(url, body)
})
await user.click(screen.getByRole('checkbox', { name: 'code-11' }))
await user.click(screen.getByRole('checkbox', { name: 'code-22' }))
await user.click(
screen.getByRole('button', { name: 'Delete selected redemption codes' })
)
const dialog = await screen.findByRole('alertdialog')
expect(dialog).toHaveAccessibleName('Delete 2 redemption codes?')
expect(remove).not.toHaveBeenCalled()
await user.click(within(dialog).getByRole('button', { name: 'Delete' }))
expect(
within(dialog).getByRole('button', { name: 'Deleting...' })
).toBeDisabled()
expect(within(dialog).getByRole('button', { name: 'Cancel' })).toBeDisabled()
release()
await waitFor(() =>
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
)
await waitFor(() =>
expect(
screen.queryByRole('checkbox', { name: 'code-11' })
).not.toBeInTheDocument()
)
expect(remove.mock.calls).toEqual([
['/api/redemption/batch', { ids: [11, 22] }],
])
expect(screen.getByRole('checkbox', { name: 'code-33' })).not.toBeChecked()
expect(screen.queryByRole('toolbar')).not.toBeInTheDocument()
expect(document.body).toHaveTextContent(
'Successfully deleted 2 redemption codes'
)
})
test.each(['server', 'network'])(
'%s failure keeps selected codes and retries with one batch request',
async (failure) => {
const { user, remove } = await setup()
if (failure === 'server') {
remove.mockResolvedValueOnce({ data: { success: false } })
} else {
remove.mockRejectedValueOnce(new Error('Network unavailable'))
}
await user.click(screen.getByRole('checkbox', { name: 'code-11' }))
await user.click(screen.getByRole('checkbox', { name: 'code-22' }))
await user.click(
screen.getByRole('button', { name: 'Delete selected redemption codes' })
)
const dialog = await screen.findByRole('alertdialog')
await user.click(within(dialog).getByRole('button', { name: 'Delete' }))
await waitFor(() =>
expect(document.body).toHaveTextContent(
'Failed to delete 2 redemption codes'
)
)
expect(dialog).toHaveAccessibleName('Delete 2 redemption codes?')
expect(screen.getByLabelText('code-11')).toBeChecked()
expect(screen.getByLabelText('code-22')).toBeChecked()
await user.click(within(dialog).getByRole('button', { name: 'Delete' }))
await waitFor(() =>
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
)
expect(remove.mock.calls).toEqual([
['/api/redemption/batch', { ids: [11, 22] }],
['/api/redemption/batch', { ids: [11, 22] }],
])
expect(screen.queryByRole('toolbar')).not.toBeInTheDocument()
}
)
...@@ -16,35 +16,77 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,35 +16,77 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { useMutation } from '@tanstack/react-query'
import type { Table } from '@tanstack/react-table' import type { Table } from '@tanstack/react-table'
import { useMemo } from 'react' import { Trash2 } from 'lucide-react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { CopyButton } from '@/components/copy-button' import { CopyButton } from '@/components/copy-button'
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table' import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
import { Button } from '@/components/ui/button'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { batchDeleteRedemptions } from '../api'
import type { Redemption } from '../types' import type { Redemption } from '../types'
import { useRedemptions } from './redemptions-provider'
type DataTableBulkActionsProps<TData> = { type DataTableBulkActionsProps = {
table: Table<TData> table: Table<Redemption>
} }
export function DataTableBulkActions<TData>({ export function DataTableBulkActions(props: DataTableBulkActionsProps) {
table,
}: DataTableBulkActionsProps<TData>) {
const { t } = useTranslation() const { t } = useTranslation()
const selectedRows = table.getSelectedRowModel().rows const { triggerRefresh } = useRedemptions()
const [deleteTargets, setDeleteTargets] = useState<Redemption[] | null>(null)
const selectedRows = props.table.getFilteredSelectedRowModel().rows
const contentToCopy = useMemo(() => { const contentToCopy = useMemo(() => {
const selectedCodes = selectedRows.map((row) => { const selectedCodes = selectedRows.map((row) => {
const redemption = row.original as Redemption const redemption = row.original
return `${redemption.name}\t${redemption.key}` return `${redemption.name}\t${redemption.key}`
}) })
return selectedCodes.join('\n') return selectedCodes.join('\n')
}, [selectedRows]) }, [selectedRows])
const deletion = useMutation({
mutationFn: async (targets: Redemption[]) => {
const result = await batchDeleteRedemptions(
targets.map((code) => code.id)
)
if (!result.success) throw new Error(result.message)
return result.data ?? 0
},
onSuccess: (count, targets) => {
toast.success(
t('Successfully deleted {{count}} redemption codes', { count })
)
props.table.setRowSelection((previous) => {
const next = { ...previous }
for (const code of targets) delete next[String(code.id)]
return next
})
setDeleteTargets(null)
triggerRefresh()
},
onError: (_error, targets) => {
toast.error(
t('Failed to delete {{count}} redemption codes', {
count: targets.length,
})
)
},
})
return ( return (
<BulkActionsToolbar table={table} entityName={t('redemption code')}> <>
<BulkActionsToolbar table={props.table} entityName={t('redemption code')}>
<CopyButton <CopyButton
value={contentToCopy} value={contentToCopy}
variant='outline' variant='outline'
...@@ -54,6 +96,47 @@ export function DataTableBulkActions<TData>({ ...@@ -54,6 +96,47 @@ export function DataTableBulkActions<TData>({
successTooltip={t('Codes copied!')} successTooltip={t('Codes copied!')}
aria-label={t('Copy selected codes')} aria-label={t('Copy selected codes')}
/> />
<Tooltip>
<TooltipTrigger
render={
<Button
variant='destructive'
size='icon'
className='size-8'
aria-label={t('Delete selected redemption codes')}
disabled={deletion.isPending}
onClick={() =>
setDeleteTargets(selectedRows.map((row) => row.original))
}
/>
}
>
<Trash2 aria-hidden='true' />
</TooltipTrigger>
<TooltipContent>
{t('Delete selected redemption codes')}
</TooltipContent>
</Tooltip>
</BulkActionsToolbar> </BulkActionsToolbar>
<ConfirmDialog
destructive
open={deleteTargets !== null}
onOpenChange={(open) => {
if (!open && !deletion.isPending) setDeleteTargets(null)
}}
title={t('Delete {{count}} redemption codes?', {
count: deleteTargets?.length ?? 0,
})}
desc={t('This action cannot be undone.')}
confirmText={deletion.isPending ? t('Deleting...') : t('Delete')}
isLoading={deletion.isPending}
disabled={!deleteTargets?.length}
handleConfirm={() => {
if (deleteTargets?.length && !deletion.isPending) {
deletion.mutate(deleteTargets)
}
}}
/>
</>
) )
} }
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useId, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Dialog } from '@/components/dialog'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
export type RedemptionExportData = {
keys: string[]
name: string
quota: string
}
type RedemptionsExportDialogProps = {
data: RedemptionExportData
onClose: () => void
}
export function RedemptionsExportDialog(props: RedemptionsExportDialogProps) {
const { t } = useTranslation()
const id = useId()
const [saveToFile, setSaveToFile] = useState(false)
const [format, setFormat] = useState<'txt' | 'md'>('txt')
const [includeName, setIncludeName] = useState(true)
const [includeQuota, setIncludeQuota] = useState(true)
const handleComplete = () => {
if (!saveToFile) {
props.onClose()
return
}
const headers: string[] = []
if (includeName) headers.push(t('Name'))
headers.push(t('Code'))
if (includeQuota) headers.push(t('Quota'))
const rows = props.data.keys.map((key) => {
const row: string[] = []
if (includeName) row.push(props.data.name)
row.push(key)
if (includeQuota) row.push(props.data.quota)
return row.map((value) => value.replaceAll(/[\t\r\n]+/g, ' '))
})
let content = `${rows.map((row) => row.join('\t')).join('\n')}\n`
if (format === 'md') {
const markdownRows = [headers, headers.map(() => '---'), ...rows]
const markdownLines = markdownRows.map((row, index) => {
const cells =
index === 1
? row
: row.map((value) =>
value
.replaceAll('&', '&amp;')
.replaceAll(/[\\`*_[\]|<>]/g, '\\$&')
)
return `| ${cells.join(' | ')} |`
})
content = `${markdownLines.join('\n')}\n`
}
const blob = new Blob([content], {
type:
format === 'md'
? 'text/markdown;charset=utf-8'
: 'text/plain;charset=utf-8',
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `redemption-codes-${Date.now()}.${format}`
document.body.append(link)
link.click()
link.remove()
URL.revokeObjectURL(url)
props.onClose()
}
return (
<Dialog
open
onOpenChange={(open) => {
if (!open) props.onClose()
}}
title={t('Redemption codes created')}
description={t('Successfully created {{count}} redemption codes', {
count: props.data.keys.length,
})}
contentClassName='sm:max-w-md'
bodyClassName='space-y-5'
footer={<Button onClick={handleComplete}>{t('Done')}</Button>}
>
<div className='flex items-center gap-2'>
<Checkbox
id={`${id}-save-file`}
checked={saveToFile}
onCheckedChange={setSaveToFile}
/>
<Label htmlFor={`${id}-save-file`}>{t('Save as a file')}</Label>
</div>
{saveToFile && (
<div className='space-y-4 pl-6'>
<RadioGroup
value={format}
onValueChange={(value) => {
if (value === 'txt' || value === 'md') {
setFormat(value)
}
}}
aria-label={t('Save redemption codes')}
className='flex flex-wrap gap-x-5 gap-y-3'
>
{[
{ value: 'txt', label: t('Save as TXT') },
{ value: 'md', label: t('Save as Markdown') },
].map((option) => (
<div key={option.value} className='flex items-center gap-2'>
<RadioGroupItem
value={option.value}
id={`${id}-${option.value}`}
/>
<Label htmlFor={`${id}-${option.value}`}>{option.label}</Label>
</div>
))}
</RadioGroup>
<div className='flex flex-wrap gap-6'>
<div className='flex items-center gap-2'>
<Checkbox
id={`${id}-name`}
checked={includeName}
onCheckedChange={setIncludeName}
/>
<Label htmlFor={`${id}-name`}>{t('Include name')}</Label>
</div>
<div className='flex items-center gap-2'>
<Checkbox
id={`${id}-quota`}
checked={includeQuota}
onCheckedChange={setIncludeQuota}
/>
<Label htmlFor={`${id}-quota`}>{t('Include quota')}</Label>
</div>
</div>
</div>
)}
</Dialog>
)
}
...@@ -50,7 +50,11 @@ import { ...@@ -50,7 +50,11 @@ import {
SheetHeader, SheetHeader,
SheetTitle, SheetTitle,
} from '@/components/ui/sheet' } from '@/components/ui/sheet'
import { getCurrencyDisplay, getCurrencyLabel } from '@/lib/currency' import {
formatQuotaWithCurrency,
getCurrencyDisplay,
getCurrencyLabel,
} from '@/lib/currency'
import { import {
formatQuota, formatQuota,
getEditableQuotaStep, getEditableQuotaStep,
...@@ -69,6 +73,10 @@ import { ...@@ -69,6 +73,10 @@ import {
transformRedemptionToFormDefaults, transformRedemptionToFormDefaults,
} from '../lib' } from '../lib'
import type { Redemption } from '../types' import type { Redemption } from '../types'
import {
RedemptionsExportDialog,
type RedemptionExportData,
} from './redemptions-export-dialog'
import { useRedemptions } from './redemptions-provider' import { useRedemptions } from './redemptions-provider'
type RedemptionsMutateDrawerProps = { type RedemptionsMutateDrawerProps = {
...@@ -87,6 +95,9 @@ export function RedemptionsMutateDrawer({ ...@@ -87,6 +95,9 @@ export function RedemptionsMutateDrawer({
const redemptionId = currentRow?.id const redemptionId = currentRow?.id
const { triggerRefresh } = useRedemptions() const { triggerRefresh } = useRedemptions()
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [createdCodes, setCreatedCodes] = useState<RedemptionExportData | null>(
null
)
const [redemptionLoadState, setRedemptionLoadState] = useState< const [redemptionLoadState, setRedemptionLoadState] = useState<
'idle' | 'loading' | 'ready' | 'error' 'idle' | 'loading' | 'ready' | 'error'
>('idle') >('idle')
...@@ -190,6 +201,15 @@ export function RedemptionsMutateDrawer({ ...@@ -190,6 +201,15 @@ export function RedemptionsMutateDrawer({
}) })
: t(SUCCESS_MESSAGES.REDEMPTION_CREATED) : t(SUCCESS_MESSAGES.REDEMPTION_CREATED)
) )
if (result.data?.length) {
setCreatedCodes({
keys: result.data,
name: basePayload.name,
quota: formatQuotaWithCurrency(basePayload.quota, {
abbreviate: false,
}),
})
}
onOpenChange(false) onOpenChange(false)
triggerRefresh() triggerRefresh()
} }
...@@ -232,6 +252,7 @@ export function RedemptionsMutateDrawer({ ...@@ -232,6 +252,7 @@ export function RedemptionsMutateDrawer({
} }
return ( return (
<>
<Sheet <Sheet
open={open} open={open}
onOpenChange={(v) => { onOpenChange={(v) => {
...@@ -241,7 +262,9 @@ export function RedemptionsMutateDrawer({ ...@@ -241,7 +262,9 @@ export function RedemptionsMutateDrawer({
} }
}} }}
> >
<SheetContent className={sideDrawerContentClassName('sm:max-w-[600px]')}> <SheetContent
className={sideDrawerContentClassName('sm:max-w-[600px]')}
>
<SheetHeader className={sideDrawerHeaderClassName()}> <SheetHeader className={sideDrawerHeaderClassName()}>
<SheetTitle> <SheetTitle>
{isUpdate {isUpdate
...@@ -423,5 +446,12 @@ export function RedemptionsMutateDrawer({ ...@@ -423,5 +446,12 @@ export function RedemptionsMutateDrawer({
</SheetFooter> </SheetFooter>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
{createdCodes && (
<RedemptionsExportDialog
data={createdCodes}
onClose={() => setCreatedCodes(null)}
/>
)}
</>
) )
} }
...@@ -133,6 +133,7 @@ export function RedemptionsTable() { ...@@ -133,6 +133,7 @@ export function RedemptionsTable() {
data: redemptions, data: redemptions,
columns, columns,
enableRowSelection: true, enableRowSelection: true,
getRowId: (row) => String(row.id),
columnFilters, columnFilters,
globalFilter, globalFilter,
pagination, pagination,
......
...@@ -617,3 +617,85 @@ it('distinguishes unchanged zero quota from missing or legacy balance metadata', ...@@ -617,3 +617,85 @@ it('distinguishes unchanged zero quota from missing or legacy balance metadata',
'legacy before → legacy after' 'legacy before → legacy after'
) )
}) })
it.each([
[
'redemption.delete_batch',
'/api/redemption/batch',
{ count: 15 },
true,
'批量删除了 15 个兑换码',
],
[
'redemption.delete_batch',
'/api/redemption/batch',
{ count: 0 },
true,
'批量删除了 0 个兑换码',
],
[
'redemption.delete',
'/api/redemption/batch',
{},
true,
'批量删除兑换码(数量未记录)',
],
[
'redemption.delete_batch',
'/api/redemption/batch',
{},
false,
'批量删除兑换码失败',
],
['redemption.delete', '/api/redemption/:id', {}, true, '删除了一个兑换码'],
])(
'renders redemption audit %s at %s with recorded result %j',
async (action, route, params, success, summary) => {
const i18n = createInstance()
await i18n.init({ lng: 'zh', resources: { zh } })
const detail = buildAuditDetails(
{
...entry,
action,
route,
method: route.endsWith('/batch') ? 'POST' : 'DELETE',
success,
other: { op: { action, params } },
},
i18n.t
)
expect(detail.summary).toBe(summary)
}
)
it('shows the affected count separately from requested redemption IDs', async () => {
const i18n = createInstance()
await i18n.init({ lng: 'en', resources: {} })
const detail = buildAuditDetails(
{
...entry,
action: 'redemption.delete_batch',
route: '/api/redemption/batch',
method: 'POST',
other: {
op: {
action: 'redemption.delete_batch',
params: {
count: 2,
total: 4,
requested_redemption_ids: [11, 12, 11, 999],
},
},
},
},
i18n.t
)
expect(detail.summary).toBe('Batch deleted 2 redemption codes')
expect(detail.fields).toEqual(
expect.arrayContaining([
{ label: 'Count', value: 2 },
{ label: 'Total', value: 4 },
{ label: 'Requested redemption code IDs', value: [11, 12, 11, 999] },
])
)
})
...@@ -87,6 +87,8 @@ export function auditFieldLabel(key: string, t: TFunction): string { ...@@ -87,6 +87,8 @@ export function auditFieldLabel(key: string, t: TFunction): string {
return t('Count') return t('Count')
case 'total': case 'total':
return t('Total') return t('Total')
case 'requested_redemption_ids':
return t('Requested redemption code IDs')
case 'requested_ids': case 'requested_ids':
return t('Requested token IDs') return t('Requested token IDs')
case 'returned_ids': case 'returned_ids':
...@@ -369,8 +371,19 @@ export function buildAuditDetails(entry: AuditLog, t: TFunction) { ...@@ -369,8 +371,19 @@ export function buildAuditDetails(entry: AuditLog, t: TFunction) {
if (entry.category === 'security') fallback = t('Account security') if (entry.category === 'security') fallback = t('Account security')
if (entry.category === 'access_token') fallback = t('Access Token') if (entry.category === 'access_token') fallback = t('Access Token')
const summary = const summary =
renderAuditContent({ op: { action, params: summaryParams } }, t) || renderAuditContent(
(entry.content && entry.content !== action ? entry.content : fallback) {
op: { action, params: summaryParams },
audit_info: {
method: entry.method,
route: entry.route,
path: entry.route,
status: entry.status,
success: entry.success,
},
},
t
) || (entry.content && entry.content !== action ? entry.content : fallback)
const admin = isAuditDetailObject(metadata.admin_info) const admin = isAuditDetailObject(metadata.admin_info)
? metadata.admin_info ? metadata.admin_info
: {} : {}
......
...@@ -510,6 +510,24 @@ export function renderAuditContent( ...@@ -510,6 +510,24 @@ export function renderAuditContent(
): string | null { ): string | null {
const op = other?.op const op = other?.op
if (!op?.action) return null if (!op?.action) return null
if (
op.action === 'redemption.delete_batch' ||
(op.action === 'redemption.delete' &&
other?.audit_info?.route === '/api/redemption/batch')
) {
if (other?.audit_info?.success === false) {
return t('Failed to batch delete redemption codes')
}
const count = op.params?.count
if (
typeof count === 'number' &&
Number.isSafeInteger(count) &&
count >= 0
) {
return t('Batch deleted {{count}} redemption codes', { count })
}
return t('Batch deleted redemption codes (count not recorded)')
}
const template = AUDIT_TEMPLATES[op.action] const template = AUDIT_TEMPLATES[op.action]
if (!template) return null if (!template) return null
const quotaOperation = buildQuotaAuditOperation( const quotaOperation = buildQuotaAuditOperation(
......
...@@ -714,6 +714,8 @@ ...@@ -714,6 +714,8 @@
"Batch delete API tokens": "Batch delete API tokens", "Batch delete API tokens": "Batch delete API tokens",
"Batch delete failed": "Batch delete failed", "Batch delete failed": "Batch delete failed",
"Batch deleted {{count}} channels": "Batch deleted {{count}} channels", "Batch deleted {{count}} channels": "Batch deleted {{count}} channels",
"Batch deleted {{count}} redemption codes": "Batch deleted {{count}} redemption codes",
"Batch deleted redemption codes (count not recorded)": "Batch deleted redemption codes (count not recorded)",
"Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed", "Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed",
"Batch detection failed": "Batch detection failed", "Batch detection failed": "Batch detection failed",
"Batch disable failed": "Batch disable failed", "Batch disable failed": "Batch disable failed",
...@@ -1499,6 +1501,7 @@ ...@@ -1499,6 +1501,7 @@
"Delete (": "Delete (", "Delete (": "Delete (",
"Delete {{count}} API key(s)?": "Delete {{count}} API key(s)?", "Delete {{count}} API key(s)?": "Delete {{count}} API key(s)?",
"Delete {{count}} models?": "Delete {{count}} models?", "Delete {{count}} models?": "Delete {{count}} models?",
"Delete {{count}} redemption codes?": "Delete {{count}} redemption codes?",
"Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.": "Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.", "Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.": "Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.",
"Delete {{count}} stale instance records? Online instances will not be deleted.": "Delete {{count}} stale instance records? Online instances will not be deleted.", "Delete {{count}} stale instance records? Online instances will not be deleted.": "Delete {{count}} stale instance records? Online instances will not be deleted.",
"Delete a runtime request header": "Delete a runtime request header", "Delete a runtime request header": "Delete a runtime request header",
...@@ -1540,6 +1543,7 @@ ...@@ -1540,6 +1543,7 @@
"Delete selected API keys": "Delete selected API keys", "Delete selected API keys": "Delete selected API keys",
"Delete selected channels": "Delete selected channels", "Delete selected channels": "Delete selected channels",
"Delete selected models": "Delete selected models", "Delete selected models": "Delete selected models",
"Delete selected redemption codes": "Delete selected redemption codes",
"Delete stale instance": "Delete stale instance", "Delete stale instance": "Delete stale instance",
"Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.", "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.",
"Delete stale instances": "Delete stale instances", "Delete stale instances": "Delete stale instances",
...@@ -2081,6 +2085,7 @@ ...@@ -2081,6 +2085,7 @@
"Failed to adjust quota": "Failed to adjust quota", "Failed to adjust quota": "Failed to adjust quota",
"Failed to apply overwrite.": "Failed to apply overwrite.", "Failed to apply overwrite.": "Failed to apply overwrite.",
"Failed to apply vendor changes": "Failed to apply vendor changes", "Failed to apply vendor changes": "Failed to apply vendor changes",
"Failed to batch delete redemption codes": "Failed to batch delete redemption codes",
"Failed to bind email": "Failed to bind email", "Failed to bind email": "Failed to bind email",
"Failed to change password": "Failed to change password", "Failed to change password": "Failed to change password",
"Failed to check for updates": "Failed to check for updates", "Failed to check for updates": "Failed to check for updates",
...@@ -2102,6 +2107,7 @@ ...@@ -2102,6 +2107,7 @@
"Failed to create redemption code": "Failed to create redemption code", "Failed to create redemption code": "Failed to create redemption code",
"Failed to create user": "Failed to create user", "Failed to create user": "Failed to create user",
"Failed to delete {{count}} model(s)": "Failed to delete {{count}} model(s)", "Failed to delete {{count}} model(s)": "Failed to delete {{count}} model(s)",
"Failed to delete {{count}} redemption codes": "Failed to delete {{count}} redemption codes",
"Failed to delete account": "Failed to delete account", "Failed to delete account": "Failed to delete account",
"Failed to delete API key": "Failed to delete API key", "Failed to delete API key": "Failed to delete API key",
"Failed to delete API keys": "Failed to delete API keys", "Failed to delete API keys": "Failed to delete API keys",
...@@ -2652,6 +2658,8 @@ ...@@ -2652,6 +2658,8 @@
"Incidents": "Incidents", "Incidents": "Incidents",
"Include Group": "Include Group", "Include Group": "Include Group",
"Include Model": "Include Model", "Include Model": "Include Model",
"Include name": "Include name",
"Include quota": "Include quota",
"Include Rule Name": "Include Rule Name", "Include Rule Name": "Include Rule Name",
"Includes request rules": "Includes request rules", "Includes request rules": "Includes request rules",
"Includes tool-call surcharge": "Includes tool-call surcharge", "Includes tool-call surcharge": "Includes tool-call surcharge",
...@@ -4266,6 +4274,7 @@ ...@@ -4266,6 +4274,7 @@
"Redemption code(s) created successfully": "Redemption code(s) created successfully", "Redemption code(s) created successfully": "Redemption code(s) created successfully",
"Redemption Codes": "Redemption Codes", "Redemption Codes": "Redemption Codes",
"Redemption codes are disabled until the administrator confirms compliance terms.": "Redemption codes are disabled until the administrator confirms compliance terms.", "Redemption codes are disabled until the administrator confirms compliance terms.": "Redemption codes are disabled until the administrator confirms compliance terms.",
"Redemption codes created": "Redemption codes created",
"redemption codes.": "redemption codes.", "redemption codes.": "redemption codes.",
"Redemption failed": "Redemption failed", "Redemption failed": "Redemption failed",
"Redemption successful! Added: {{quota}}": "Redemption successful! Added: {{quota}}", "Redemption successful! Added: {{quota}}": "Redemption successful! Added: {{quota}}",
...@@ -4406,6 +4415,7 @@ ...@@ -4406,6 +4415,7 @@
"Requested items": "Requested items", "Requested items": "Requested items",
"Requested quota": "Requested quota", "Requested quota": "Requested quota",
"Requested quota: {{quota}}": "Requested quota: {{quota}}", "Requested quota: {{quota}}": "Requested quota: {{quota}}",
"Requested redemption code IDs": "Requested redemption code IDs",
"Requested token IDs": "Requested token IDs", "Requested token IDs": "Requested token IDs",
"Requested token IDs truncated": "Requested token IDs truncated", "Requested token IDs truncated": "Requested token IDs truncated",
"Requested: {{total}}": "Requested: {{total}}", "Requested: {{total}}": "Requested: {{total}}",
...@@ -4589,6 +4599,9 @@ ...@@ -4589,6 +4599,9 @@
"Save": "Save", "Save": "Save",
"Save & Submit": "Save & Submit", "Save & Submit": "Save & Submit",
"Save all settings": "Save all settings", "Save all settings": "Save all settings",
"Save as a file": "Save as a file",
"Save as Markdown": "Save as Markdown",
"Save as TXT": "Save as TXT",
"Save Backup Codes": "Save Backup Codes", "Save Backup Codes": "Save Backup Codes",
"Save changes": "Save changes", "Save changes": "Save changes",
"Save Changes": "Save Changes", "Save Changes": "Save Changes",
...@@ -4614,6 +4627,7 @@ ...@@ -4614,6 +4627,7 @@
"Save Preferences": "Save Preferences", "Save Preferences": "Save Preferences",
"Save preview": "Save preview", "Save preview": "Save preview",
"Save rate limits": "Save rate limits", "Save rate limits": "Save rate limits",
"Save redemption codes": "Save redemption codes",
"Save sensitive words": "Save sensitive words", "Save sensitive words": "Save sensitive words",
"Save Settings": "Save Settings", "Save Settings": "Save Settings",
"Save sidebar modules": "Save sidebar modules", "Save sidebar modules": "Save sidebar modules",
...@@ -5056,6 +5070,7 @@ ...@@ -5056,6 +5070,7 @@
"Successfully deleted {{count}} API key(s)": "Successfully deleted {{count}} API key(s)", "Successfully deleted {{count}} API key(s)": "Successfully deleted {{count}} API key(s)",
"Successfully deleted {{count}} invalid redemption codes": "Successfully deleted {{count}} invalid redemption codes", "Successfully deleted {{count}} invalid redemption codes": "Successfully deleted {{count}} invalid redemption codes",
"Successfully deleted {{count}} model(s)": "Successfully deleted {{count}} model(s)", "Successfully deleted {{count}} model(s)": "Successfully deleted {{count}} model(s)",
"Successfully deleted {{count}} redemption codes": "Successfully deleted {{count}} redemption codes",
"Successfully disabled {{count}} model(s)": "Successfully disabled {{count}} model(s)", "Successfully disabled {{count}} model(s)": "Successfully disabled {{count}} model(s)",
"Successfully enabled {{count}} model(s)": "Successfully enabled {{count}} model(s)", "Successfully enabled {{count}} model(s)": "Successfully enabled {{count}} model(s)",
"Suffix": "Suffix", "Suffix": "Suffix",
......
...@@ -714,6 +714,8 @@ ...@@ -714,6 +714,8 @@
"Batch delete API tokens": "Supprimer des jetons API par lot", "Batch delete API tokens": "Supprimer des jetons API par lot",
"Batch delete failed": "Échec de la suppression par lots", "Batch delete failed": "Échec de la suppression par lots",
"Batch deleted {{count}} channels": "{{count}} canaux supprimés par lot", "Batch deleted {{count}} channels": "{{count}} canaux supprimés par lot",
"Batch deleted {{count}} redemption codes": "{{count}} codes d’échange supprimés en lot",
"Batch deleted redemption codes (count not recorded)": "Codes d’échange supprimés en lot (nombre non enregistré)",
"Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "Détection par lots terminée : {{channels}} canaux, {{add}} à ajouter, {{remove}} à supprimer, {{fails}} échoués", "Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "Détection par lots terminée : {{channels}} canaux, {{add}} à ajouter, {{remove}} à supprimer, {{fails}} échoués",
"Batch detection failed": "Échec de la détection par lot", "Batch detection failed": "Échec de la détection par lot",
"Batch disable failed": "Échec de la désactivation par lots", "Batch disable failed": "Échec de la désactivation par lots",
...@@ -1499,6 +1501,7 @@ ...@@ -1499,6 +1501,7 @@
"Delete (": "Supprimer (", "Delete (": "Supprimer (",
"Delete {{count}} API key(s)?": "Supprimer {{count}} clé(s) API ?", "Delete {{count}} API key(s)?": "Supprimer {{count}} clé(s) API ?",
"Delete {{count}} models?": "Supprimer {{count}} modèles ?", "Delete {{count}} models?": "Supprimer {{count}} modèles ?",
"Delete {{count}} redemption codes?": "Supprimer {{count}} codes d’échange ?",
"Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.": "Supprimer les {{count}} fournisseurs sélectionnés ? Ceux ayant des modèles associés ne peuvent pas être supprimés.", "Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.": "Supprimer les {{count}} fournisseurs sélectionnés ? Ceux ayant des modèles associés ne peuvent pas être supprimés.",
"Delete {{count}} stale instance records? Online instances will not be deleted.": "Supprimer {{count}} enregistrement(s) d'instance expirée ? Les instances en ligne ne seront pas supprimées.", "Delete {{count}} stale instance records? Online instances will not be deleted.": "Supprimer {{count}} enregistrement(s) d'instance expirée ? Les instances en ligne ne seront pas supprimées.",
"Delete a runtime request header": "Supprimer un en-tête de requête à l'exécution", "Delete a runtime request header": "Supprimer un en-tête de requête à l'exécution",
...@@ -1540,6 +1543,7 @@ ...@@ -1540,6 +1543,7 @@
"Delete selected API keys": "Supprimer les clés API sélectionnées", "Delete selected API keys": "Supprimer les clés API sélectionnées",
"Delete selected channels": "Supprimer les canaux sélectionnés", "Delete selected channels": "Supprimer les canaux sélectionnés",
"Delete selected models": "Supprimer les modèles sélectionnés", "Delete selected models": "Supprimer les modèles sélectionnés",
"Delete selected redemption codes": "Supprimer les codes sélectionnés",
"Delete stale instance": "Supprimer l'instance expirée", "Delete stale instance": "Supprimer l'instance expirée",
"Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Supprimer l'instance expirée \"{{name}}\" ? Si elle a de nouveau signalé son état, elle ne sera pas supprimée.", "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Supprimer l'instance expirée \"{{name}}\" ? Si elle a de nouveau signalé son état, elle ne sera pas supprimée.",
"Delete stale instances": "Supprimer les instances expirées", "Delete stale instances": "Supprimer les instances expirées",
...@@ -2081,6 +2085,7 @@ ...@@ -2081,6 +2085,7 @@
"Failed to adjust quota": "Échec de l'ajustement du quota", "Failed to adjust quota": "Échec de l'ajustement du quota",
"Failed to apply overwrite.": "Échec de l'application de l'écrasement.", "Failed to apply overwrite.": "Échec de l'application de l'écrasement.",
"Failed to apply vendor changes": "Échec de l’application des modifications", "Failed to apply vendor changes": "Échec de l’application des modifications",
"Failed to batch delete redemption codes": "Échec de la suppression des codes d’échange en lot",
"Failed to bind email": "Échec de la liaison de l'e-mail", "Failed to bind email": "Échec de la liaison de l'e-mail",
"Failed to change password": "Échec du changement de mot de passe", "Failed to change password": "Échec du changement de mot de passe",
"Failed to check for updates": "Échec de la vérification des mises à jour", "Failed to check for updates": "Échec de la vérification des mises à jour",
...@@ -2102,6 +2107,7 @@ ...@@ -2102,6 +2107,7 @@
"Failed to create redemption code": "Échec de la création du code d'échange", "Failed to create redemption code": "Échec de la création du code d'échange",
"Failed to create user": "Échec de la création de l'utilisateur", "Failed to create user": "Échec de la création de l'utilisateur",
"Failed to delete {{count}} model(s)": "Échec de la suppression de {{count}} modèle(s)", "Failed to delete {{count}} model(s)": "Échec de la suppression de {{count}} modèle(s)",
"Failed to delete {{count}} redemption codes": "Échec de la suppression de {{count}} codes d’échange",
"Failed to delete account": "Échec de la suppression du compte", "Failed to delete account": "Échec de la suppression du compte",
"Failed to delete API key": "Échec de la suppression de la clé API", "Failed to delete API key": "Échec de la suppression de la clé API",
"Failed to delete API keys": "Échec de la suppression des Clés API", "Failed to delete API keys": "Échec de la suppression des Clés API",
...@@ -2652,6 +2658,8 @@ ...@@ -2652,6 +2658,8 @@
"Incidents": "Incidents", "Incidents": "Incidents",
"Include Group": "Inclure le groupe", "Include Group": "Inclure le groupe",
"Include Model": "Inclure le modèle", "Include Model": "Inclure le modèle",
"Include name": "Inclure le nom",
"Include quota": "Inclure le quota",
"Include Rule Name": "Inclure le nom de la règle", "Include Rule Name": "Inclure le nom de la règle",
"Includes request rules": "Inclut des règles de requête", "Includes request rules": "Inclut des règles de requête",
"Includes tool-call surcharge": "Inclut un supplément pour appel d’outil", "Includes tool-call surcharge": "Inclut un supplément pour appel d’outil",
...@@ -4266,6 +4274,7 @@ ...@@ -4266,6 +4274,7 @@
"Redemption code(s) created successfully": "Code(s) d'échange créé(s) avec succès", "Redemption code(s) created successfully": "Code(s) d'échange créé(s) avec succès",
"Redemption Codes": "Codes d'échange", "Redemption Codes": "Codes d'échange",
"Redemption codes are disabled until the administrator confirms compliance terms.": "Les codes de兑换 sont désactivés jusqu’à ce que l’administrateur confirme les conditions de conformité.", "Redemption codes are disabled until the administrator confirms compliance terms.": "Les codes de兑换 sont désactivés jusqu’à ce que l’administrateur confirme les conditions de conformité.",
"Redemption codes created": "Codes d’échange créés",
"redemption codes.": "codes de rachat.", "redemption codes.": "codes de rachat.",
"Redemption failed": "Rédemption échouée", "Redemption failed": "Rédemption échouée",
"Redemption successful! Added: {{quota}}": "Échange réussi ! Ajouté : {{quota}}", "Redemption successful! Added: {{quota}}": "Échange réussi ! Ajouté : {{quota}}",
...@@ -4406,6 +4415,7 @@ ...@@ -4406,6 +4415,7 @@
"Requested items": "Éléments demandés", "Requested items": "Éléments demandés",
"Requested quota": "Montant demandé", "Requested quota": "Montant demandé",
"Requested quota: {{quota}}": "Montant demandé : {{quota}}", "Requested quota: {{quota}}": "Montant demandé : {{quota}}",
"Requested redemption code IDs": "ID des codes d’échange à supprimer",
"Requested token IDs": "ID des jetons demandés", "Requested token IDs": "ID des jetons demandés",
"Requested token IDs truncated": "Liste des ID demandés tronquée", "Requested token IDs truncated": "Liste des ID demandés tronquée",
"Requested: {{total}}": "Demandés : {{total}}", "Requested: {{total}}": "Demandés : {{total}}",
...@@ -4589,6 +4599,9 @@ ...@@ -4589,6 +4599,9 @@
"Save": "Enregistrer", "Save": "Enregistrer",
"Save & Submit": "Enregistrer et envoyer", "Save & Submit": "Enregistrer et envoyer",
"Save all settings": "Enregistrer tous les paramètres", "Save all settings": "Enregistrer tous les paramètres",
"Save as a file": "Enregistrer dans un fichier",
"Save as Markdown": "Enregistrer en Markdown",
"Save as TXT": "Enregistrer en TXT",
"Save Backup Codes": "Sauvegarder les codes de secours", "Save Backup Codes": "Sauvegarder les codes de secours",
"Save changes": "Enregistrer les modifications", "Save changes": "Enregistrer les modifications",
"Save Changes": "Enregistrer les modifications", "Save Changes": "Enregistrer les modifications",
...@@ -4614,6 +4627,7 @@ ...@@ -4614,6 +4627,7 @@
"Save Preferences": "Enregistrer les préférences", "Save Preferences": "Enregistrer les préférences",
"Save preview": "Aperçu de l’enregistrement", "Save preview": "Aperçu de l’enregistrement",
"Save rate limits": "Enregistrer les limites de débit", "Save rate limits": "Enregistrer les limites de débit",
"Save redemption codes": "Enregistrer les codes d’échange",
"Save sensitive words": "Enregistrer les mots sensibles", "Save sensitive words": "Enregistrer les mots sensibles",
"Save Settings": "Enregistrer les paramètres", "Save Settings": "Enregistrer les paramètres",
"Save sidebar modules": "Enregistrer les modules de la barre latérale", "Save sidebar modules": "Enregistrer les modules de la barre latérale",
...@@ -5056,6 +5070,7 @@ ...@@ -5056,6 +5070,7 @@
"Successfully deleted {{count}} API key(s)": "{{count}} clé(s) API supprimée(s) avec succès", "Successfully deleted {{count}} API key(s)": "{{count}} clé(s) API supprimée(s) avec succès",
"Successfully deleted {{count}} invalid redemption codes": "{{count}} code(s) d'échange invalide(s) supprimé(s) avec succès", "Successfully deleted {{count}} invalid redemption codes": "{{count}} code(s) d'échange invalide(s) supprimé(s) avec succès",
"Successfully deleted {{count}} model(s)": "{{count}} modèle(s) supprimé(s) avec succès", "Successfully deleted {{count}} model(s)": "{{count}} modèle(s) supprimé(s) avec succès",
"Successfully deleted {{count}} redemption codes": "{{count}} codes d’échange supprimés",
"Successfully disabled {{count}} model(s)": "{{count}} modèle(s) désactivé(s) avec succès", "Successfully disabled {{count}} model(s)": "{{count}} modèle(s) désactivé(s) avec succès",
"Successfully enabled {{count}} model(s)": "{{count}} modèle(s) activé(s) avec succès", "Successfully enabled {{count}} model(s)": "{{count}} modèle(s) activé(s) avec succès",
"Suffix": "Suffixe", "Suffix": "Suffixe",
......
...@@ -714,6 +714,8 @@ ...@@ -714,6 +714,8 @@
"Batch delete API tokens": "API トークンを一括削除", "Batch delete API tokens": "API トークンを一括削除",
"Batch delete failed": "一括削除に失敗しました", "Batch delete failed": "一括削除に失敗しました",
"Batch deleted {{count}} channels": "{{count}} 件のチャネルを一括削除しました", "Batch deleted {{count}} channels": "{{count}} 件のチャネルを一括削除しました",
"Batch deleted {{count}} redemption codes": "{{count}} 件の引き換えコードを一括削除しました",
"Batch deleted redemption codes (count not recorded)": "引き換えコードを一括削除(件数未記録)",
"Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "一括検出完了:{{channels}} チャネル、{{add}} 個追加、{{remove}} 個削除、{{fails}} 個失敗", "Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "一括検出完了:{{channels}} チャネル、{{add}} 個追加、{{remove}} 個削除、{{fails}} 個失敗",
"Batch detection failed": "一括検出に失敗しました", "Batch detection failed": "一括検出に失敗しました",
"Batch disable failed": "一括無効化に失敗しました", "Batch disable failed": "一括無効化に失敗しました",
...@@ -1499,6 +1501,7 @@ ...@@ -1499,6 +1501,7 @@
"Delete (": "削除 (", "Delete (": "削除 (",
"Delete {{count}} API key(s)?": "{{count}}個のAPIキーを削除しますか?", "Delete {{count}} API key(s)?": "{{count}}個のAPIキーを削除しますか?",
"Delete {{count}} models?": "{{count}} 件のモデルを削除しますか?", "Delete {{count}} models?": "{{count}} 件のモデルを削除しますか?",
"Delete {{count}} redemption codes?": "{{count}} 件の引き換えコードを削除しますか?",
"Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.": "選択した {{count}} 件のプロバイダーを削除しますか?関連モデルがある場合は削除できません。", "Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.": "選択した {{count}} 件のプロバイダーを削除しますか?関連モデルがある場合は削除できません。",
"Delete {{count}} stale instance records? Online instances will not be deleted.": "期限切れインスタンスレコードを {{count}} 件削除しますか?オンラインのインスタンスは削除されません。", "Delete {{count}} stale instance records? Online instances will not be deleted.": "期限切れインスタンスレコードを {{count}} 件削除しますか?オンラインのインスタンスは削除されません。",
"Delete a runtime request header": "ランタイムリクエストヘッダーを削除", "Delete a runtime request header": "ランタイムリクエストヘッダーを削除",
...@@ -1540,6 +1543,7 @@ ...@@ -1540,6 +1543,7 @@
"Delete selected API keys": "選択したAPIキーを削除", "Delete selected API keys": "選択したAPIキーを削除",
"Delete selected channels": "選択したチャネルを削除", "Delete selected channels": "選択したチャネルを削除",
"Delete selected models": "選択したモデルを削除", "Delete selected models": "選択したモデルを削除",
"Delete selected redemption codes": "選択した引き換えコードを削除",
"Delete stale instance": "期限切れインスタンスを削除", "Delete stale instance": "期限切れインスタンスを削除",
"Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "期限切れインスタンス \"{{name}}\" を削除しますか?再度報告されている場合は削除されません。", "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "期限切れインスタンス \"{{name}}\" を削除しますか?再度報告されている場合は削除されません。",
"Delete stale instances": "期限切れインスタンスを削除", "Delete stale instances": "期限切れインスタンスを削除",
...@@ -2081,6 +2085,7 @@ ...@@ -2081,6 +2085,7 @@
"Failed to adjust quota": "クォータの調整に失敗しました", "Failed to adjust quota": "クォータの調整に失敗しました",
"Failed to apply overwrite.": "オーバーライトの適用に失敗しました。", "Failed to apply overwrite.": "オーバーライトの適用に失敗しました。",
"Failed to apply vendor changes": "プロバイダーの変更を適用できませんでした", "Failed to apply vendor changes": "プロバイダーの変更を適用できませんでした",
"Failed to batch delete redemption codes": "引き換えコードの一括削除に失敗しました",
"Failed to bind email": "メールのバインドに失敗しました", "Failed to bind email": "メールのバインドに失敗しました",
"Failed to change password": "パスワードの変更に失敗しました", "Failed to change password": "パスワードの変更に失敗しました",
"Failed to check for updates": "更新の確認に失敗しました", "Failed to check for updates": "更新の確認に失敗しました",
...@@ -2102,6 +2107,7 @@ ...@@ -2102,6 +2107,7 @@
"Failed to create redemption code": "引き換えコードの作成に失敗しました", "Failed to create redemption code": "引き換えコードの作成に失敗しました",
"Failed to create user": "ユーザーの作成に失敗しました", "Failed to create user": "ユーザーの作成に失敗しました",
"Failed to delete {{count}} model(s)": "{{count}} 個のモデルの削除に失敗しました", "Failed to delete {{count}} model(s)": "{{count}} 個のモデルの削除に失敗しました",
"Failed to delete {{count}} redemption codes": "{{count}} 件の引き換えコードを削除できませんでした",
"Failed to delete account": "アカウントの削除に失敗しました", "Failed to delete account": "アカウントの削除に失敗しました",
"Failed to delete API key": "APIキーの削除に失敗しました", "Failed to delete API key": "APIキーの削除に失敗しました",
"Failed to delete API keys": "APIキーの削除に失敗しました", "Failed to delete API keys": "APIキーの削除に失敗しました",
...@@ -2652,6 +2658,8 @@ ...@@ -2652,6 +2658,8 @@
"Incidents": "インシデント", "Incidents": "インシデント",
"Include Group": "グループを含む", "Include Group": "グループを含む",
"Include Model": "モデルを含む", "Include Model": "モデルを含む",
"Include name": "名前を含める",
"Include quota": "クォータを含める",
"Include Rule Name": "ルール名を含む", "Include Rule Name": "ルール名を含む",
"Includes request rules": "リクエストルールを含む", "Includes request rules": "リクエストルールを含む",
"Includes tool-call surcharge": "ツール呼び出しの追加料金を含む", "Includes tool-call surcharge": "ツール呼び出しの追加料金を含む",
...@@ -4266,6 +4274,7 @@ ...@@ -4266,6 +4274,7 @@
"Redemption code(s) created successfully": "引き換えコードが正常に作成されました", "Redemption code(s) created successfully": "引き換えコードが正常に作成されました",
"Redemption Codes": "引き換えコード", "Redemption Codes": "引き換えコード",
"Redemption codes are disabled until the administrator confirms compliance terms.": "管理者がコンプライアンス条件を確認するまで、引換コードは無効です。", "Redemption codes are disabled until the administrator confirms compliance terms.": "管理者がコンプライアンス条件を確認するまで、引換コードは無効です。",
"Redemption codes created": "引き換えコードを作成しました",
"redemption codes.": "引き換えコード。", "redemption codes.": "引き換えコード。",
"Redemption failed": "交換に失敗しました", "Redemption failed": "交換に失敗しました",
"Redemption successful! Added: {{quota}}": "引き換え成功!追加:{{quota}}", "Redemption successful! Added: {{quota}}": "引き換え成功!追加:{{quota}}",
...@@ -4406,6 +4415,7 @@ ...@@ -4406,6 +4415,7 @@
"Requested items": "リクエスト件数", "Requested items": "リクエスト件数",
"Requested quota": "リクエスト額", "Requested quota": "リクエスト額",
"Requested quota: {{quota}}": "リクエスト額:{{quota}}", "Requested quota: {{quota}}": "リクエスト額:{{quota}}",
"Requested redemption code IDs": "削除を要求した引き換えコードの ID",
"Requested token IDs": "リクエストしたトークン ID", "Requested token IDs": "リクエストしたトークン ID",
"Requested token IDs truncated": "リクエストしたトークン ID の一覧は省略されています", "Requested token IDs truncated": "リクエストしたトークン ID の一覧は省略されています",
"Requested: {{total}}": "リクエスト:{{total}} 件", "Requested: {{total}}": "リクエスト:{{total}} 件",
...@@ -4589,6 +4599,9 @@ ...@@ -4589,6 +4599,9 @@
"Save": "保存", "Save": "保存",
"Save & Submit": "保存して送信", "Save & Submit": "保存して送信",
"Save all settings": "すべての設定を保存", "Save all settings": "すべての設定を保存",
"Save as a file": "ファイルに保存",
"Save as Markdown": "Markdown で保存",
"Save as TXT": "TXT で保存",
"Save Backup Codes": "バックアップコードを保存", "Save Backup Codes": "バックアップコードを保存",
"Save changes": "変更を保存", "Save changes": "変更を保存",
"Save Changes": "変更を保存", "Save Changes": "変更を保存",
...@@ -4614,6 +4627,7 @@ ...@@ -4614,6 +4627,7 @@
"Save Preferences": "設定を保存", "Save Preferences": "設定を保存",
"Save preview": "保存プレビュー", "Save preview": "保存プレビュー",
"Save rate limits": "レート制限を保存", "Save rate limits": "レート制限を保存",
"Save redemption codes": "引き換えコードを保存",
"Save sensitive words": "敏感な言葉を保存", "Save sensitive words": "敏感な言葉を保存",
"Save Settings": "設定を保存", "Save Settings": "設定を保存",
"Save sidebar modules": "サイドバーモジュールを保存", "Save sidebar modules": "サイドバーモジュールを保存",
...@@ -5056,6 +5070,7 @@ ...@@ -5056,6 +5070,7 @@
"Successfully deleted {{count}} API key(s)": "{{count}}個のAPIキーが正常に削除されました", "Successfully deleted {{count}} API key(s)": "{{count}}個のAPIキーが正常に削除されました",
"Successfully deleted {{count}} invalid redemption codes": "{{count}} 件の無効な引き換えコードを削除しました", "Successfully deleted {{count}} invalid redemption codes": "{{count}} 件の無効な引き換えコードを削除しました",
"Successfully deleted {{count}} model(s)": "{{count}} 個のモデルを削除しました", "Successfully deleted {{count}} model(s)": "{{count}} 個のモデルを削除しました",
"Successfully deleted {{count}} redemption codes": "{{count}} 件の引き換えコードを削除しました",
"Successfully disabled {{count}} model(s)": "{{count}} 個のモデルを無効にしました", "Successfully disabled {{count}} model(s)": "{{count}} 個のモデルを無効にしました",
"Successfully enabled {{count}} model(s)": "{{count}} 個のモデルを有効にしました", "Successfully enabled {{count}} model(s)": "{{count}} 個のモデルを有効にしました",
"Suffix": "サフィックス", "Suffix": "サフィックス",
......
...@@ -714,6 +714,8 @@ ...@@ -714,6 +714,8 @@
"Batch delete API tokens": "Массовое удаление токенов API", "Batch delete API tokens": "Массовое удаление токенов API",
"Batch delete failed": "Пакетное удаление не удалось", "Batch delete failed": "Пакетное удаление не удалось",
"Batch deleted {{count}} channels": "Пакетно удалено каналов: {{count}}", "Batch deleted {{count}} channels": "Пакетно удалено каналов: {{count}}",
"Batch deleted {{count}} redemption codes": "Массово удалено кодов погашения: {{count}}",
"Batch deleted redemption codes (count not recorded)": "Массовое удаление кодов погашения (количество не записано)",
"Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "Пакетное обнаружение завершено: {{channels}} каналов, {{add}} для добавления, {{remove}} для удаления, {{fails}} ошибок", "Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "Пакетное обнаружение завершено: {{channels}} каналов, {{add}} для добавления, {{remove}} для удаления, {{fails}} ошибок",
"Batch detection failed": "Пакетное обнаружение не удалось", "Batch detection failed": "Пакетное обнаружение не удалось",
"Batch disable failed": "Пакетное отключение не удалось", "Batch disable failed": "Пакетное отключение не удалось",
...@@ -1499,6 +1501,7 @@ ...@@ -1499,6 +1501,7 @@
"Delete (": "Удалить (", "Delete (": "Удалить (",
"Delete {{count}} API key(s)?": "Удалить {{count}} API-ключ(а/ей)?", "Delete {{count}} API key(s)?": "Удалить {{count}} API-ключ(а/ей)?",
"Delete {{count}} models?": "Удалить {{count}} моделей?", "Delete {{count}} models?": "Удалить {{count}} моделей?",
"Delete {{count}} redemption codes?": "Удалить коды погашения ({{count}})?",
"Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.": "Удалить выбранные записи поставщиков ({{count}})? Поставщиков со связанными моделями удалить нельзя.", "Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.": "Удалить выбранные записи поставщиков ({{count}})? Поставщиков со связанными моделями удалить нельзя.",
"Delete {{count}} stale instance records? Online instances will not be deleted.": "Удалить {{count}} записей устаревших экземпляров? Онлайн-экземпляры не будут удалены.", "Delete {{count}} stale instance records? Online instances will not be deleted.": "Удалить {{count}} записей устаревших экземпляров? Онлайн-экземпляры не будут удалены.",
"Delete a runtime request header": "Удалить заголовок запроса во время выполнения", "Delete a runtime request header": "Удалить заголовок запроса во время выполнения",
...@@ -1540,6 +1543,7 @@ ...@@ -1540,6 +1543,7 @@
"Delete selected API keys": "Удалить выбранные ключи API", "Delete selected API keys": "Удалить выбранные ключи API",
"Delete selected channels": "Удалить выбранные каналы", "Delete selected channels": "Удалить выбранные каналы",
"Delete selected models": "Удалить выбранные модели", "Delete selected models": "Удалить выбранные модели",
"Delete selected redemption codes": "Удалить выбранные коды погашения",
"Delete stale instance": "Удалить устаревший экземпляр", "Delete stale instance": "Удалить устаревший экземпляр",
"Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Удалить устаревший экземпляр \"{{name}}\"? Если он снова отправил отчет, он не будет удален.", "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Удалить устаревший экземпляр \"{{name}}\"? Если он снова отправил отчет, он не будет удален.",
"Delete stale instances": "Удалить устаревшие экземпляры", "Delete stale instances": "Удалить устаревшие экземпляры",
...@@ -2081,6 +2085,7 @@ ...@@ -2081,6 +2085,7 @@
"Failed to adjust quota": "Не удалось изменить квоту", "Failed to adjust quota": "Не удалось изменить квоту",
"Failed to apply overwrite.": "Не удалось применить перезапись.", "Failed to apply overwrite.": "Не удалось применить перезапись.",
"Failed to apply vendor changes": "Не удалось применить изменения поставщиков", "Failed to apply vendor changes": "Не удалось применить изменения поставщиков",
"Failed to batch delete redemption codes": "Не удалось массово удалить коды погашения",
"Failed to bind email": "Не удалось привязать email", "Failed to bind email": "Не удалось привязать email",
"Failed to change password": "Не удалось изменить пароль", "Failed to change password": "Не удалось изменить пароль",
"Failed to check for updates": "Не удалось проверить обновления", "Failed to check for updates": "Не удалось проверить обновления",
...@@ -2102,6 +2107,7 @@ ...@@ -2102,6 +2107,7 @@
"Failed to create redemption code": "Не удалось создать код активации", "Failed to create redemption code": "Не удалось создать код активации",
"Failed to create user": "Не удалось создать пользователя", "Failed to create user": "Не удалось создать пользователя",
"Failed to delete {{count}} model(s)": "Не удалось удалить {{count}} моделей", "Failed to delete {{count}} model(s)": "Не удалось удалить {{count}} моделей",
"Failed to delete {{count}} redemption codes": "Не удалось удалить коды погашения: {{count}}",
"Failed to delete account": "Не удалось удалить аккаунт", "Failed to delete account": "Не удалось удалить аккаунт",
"Failed to delete API key": "Не удалось удалить API ключ", "Failed to delete API key": "Не удалось удалить API ключ",
"Failed to delete API keys": "Не удалось удалить API ключи", "Failed to delete API keys": "Не удалось удалить API ключи",
...@@ -2652,6 +2658,8 @@ ...@@ -2652,6 +2658,8 @@
"Incidents": "Инциденты", "Incidents": "Инциденты",
"Include Group": "Включить группу", "Include Group": "Включить группу",
"Include Model": "Включить модель", "Include Model": "Включить модель",
"Include name": "Включить название",
"Include quota": "Включить квоту",
"Include Rule Name": "Включить имя правила", "Include Rule Name": "Включить имя правила",
"Includes request rules": "Включает правила запросов", "Includes request rules": "Включает правила запросов",
"Includes tool-call surcharge": "Включает доплату за вызов инструмента", "Includes tool-call surcharge": "Включает доплату за вызов инструмента",
...@@ -4266,6 +4274,7 @@ ...@@ -4266,6 +4274,7 @@
"Redemption code(s) created successfully": "Код(ы) активации успешно создан(ы)", "Redemption code(s) created successfully": "Код(ы) активации успешно создан(ы)",
"Redemption Codes": "Коды активации", "Redemption Codes": "Коды активации",
"Redemption codes are disabled until the administrator confirms compliance terms.": "Коды пополнения отключены, пока администратор не подтвердит условия соответствия.", "Redemption codes are disabled until the administrator confirms compliance terms.": "Коды пополнения отключены, пока администратор не подтвердит условия соответствия.",
"Redemption codes created": "Коды погашения созданы",
"redemption codes.": "коды активации.", "redemption codes.": "коды активации.",
"Redemption failed": "Погашение не удалось", "Redemption failed": "Погашение не удалось",
"Redemption successful! Added: {{quota}}": "Погашение успешно! Добавлено: {{quota}}", "Redemption successful! Added: {{quota}}": "Погашение успешно! Добавлено: {{quota}}",
...@@ -4406,6 +4415,7 @@ ...@@ -4406,6 +4415,7 @@
"Requested items": "Запрошенные элементы", "Requested items": "Запрошенные элементы",
"Requested quota": "Запрошенная сумма", "Requested quota": "Запрошенная сумма",
"Requested quota: {{quota}}": "Запрошенная сумма: {{quota}}", "Requested quota: {{quota}}": "Запрошенная сумма: {{quota}}",
"Requested redemption code IDs": "ID кодов погашения, запрошенных к удалению",
"Requested token IDs": "Запрошенные ID токенов", "Requested token IDs": "Запрошенные ID токенов",
"Requested token IDs truncated": "Список запрошенных ID токенов усечён", "Requested token IDs truncated": "Список запрошенных ID токенов усечён",
"Requested: {{total}}": "Запрошено: {{total}}", "Requested: {{total}}": "Запрошено: {{total}}",
...@@ -4589,6 +4599,9 @@ ...@@ -4589,6 +4599,9 @@
"Save": "Сохранить", "Save": "Сохранить",
"Save & Submit": "Сохранить и отправить", "Save & Submit": "Сохранить и отправить",
"Save all settings": "Сохранить все настройки", "Save all settings": "Сохранить все настройки",
"Save as a file": "Сохранить в файл",
"Save as Markdown": "Сохранить в Markdown",
"Save as TXT": "Сохранить в TXT",
"Save Backup Codes": "Сохранить резервные коды", "Save Backup Codes": "Сохранить резервные коды",
"Save changes": "Сохранить изменения", "Save changes": "Сохранить изменения",
"Save Changes": "Сохранить изменения", "Save Changes": "Сохранить изменения",
...@@ -4614,6 +4627,7 @@ ...@@ -4614,6 +4627,7 @@
"Save Preferences": "Сохранить настройки", "Save Preferences": "Сохранить настройки",
"Save preview": "Предпросмотр сохранения", "Save preview": "Предпросмотр сохранения",
"Save rate limits": "Сохранить лимиты скорости", "Save rate limits": "Сохранить лимиты скорости",
"Save redemption codes": "Сохранить коды погашения",
"Save sensitive words": "Сохранить чувствительные слова", "Save sensitive words": "Сохранить чувствительные слова",
"Save Settings": "Сохранить настройки", "Save Settings": "Сохранить настройки",
"Save sidebar modules": "Сохранить модули боковой панели", "Save sidebar modules": "Сохранить модули боковой панели",
...@@ -5056,6 +5070,7 @@ ...@@ -5056,6 +5070,7 @@
"Successfully deleted {{count}} API key(s)": "Успешно удалено {{count}} API-ключ(а/ей)", "Successfully deleted {{count}} API key(s)": "Успешно удалено {{count}} API-ключ(а/ей)",
"Successfully deleted {{count}} invalid redemption codes": "Успешно удалено {{count}} недействительных кодов активации", "Successfully deleted {{count}} invalid redemption codes": "Успешно удалено {{count}} недействительных кодов активации",
"Successfully deleted {{count}} model(s)": "Успешно удалено {{count}} моделей", "Successfully deleted {{count}} model(s)": "Успешно удалено {{count}} моделей",
"Successfully deleted {{count}} redemption codes": "Коды погашения удалены: {{count}}",
"Successfully disabled {{count}} model(s)": "Успешно отключено {{count}} моделей", "Successfully disabled {{count}} model(s)": "Успешно отключено {{count}} моделей",
"Successfully enabled {{count}} model(s)": "Успешно включено {{count}} моделей", "Successfully enabled {{count}} model(s)": "Успешно включено {{count}} моделей",
"Suffix": "Суффикс", "Suffix": "Суффикс",
......
...@@ -714,6 +714,8 @@ ...@@ -714,6 +714,8 @@
"Batch delete API tokens": "Xóa hàng loạt mã thông báo API", "Batch delete API tokens": "Xóa hàng loạt mã thông báo API",
"Batch delete failed": "Xóa hàng loạt thất bại", "Batch delete failed": "Xóa hàng loạt thất bại",
"Batch deleted {{count}} channels": "Đã xóa hàng loạt {{count}} kênh", "Batch deleted {{count}} channels": "Đã xóa hàng loạt {{count}} kênh",
"Batch deleted {{count}} redemption codes": "Đã xóa hàng loạt {{count}} mã quy đổi",
"Batch deleted redemption codes (count not recorded)": "Đã xóa hàng loạt mã quy đổi (không ghi lại số lượng)",
"Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "Phát hiện hàng loạt hoàn tất: {{channels}} kênh, {{add}} để thêm, {{remove}} để xóa, {{fails}} thất bại", "Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "Phát hiện hàng loạt hoàn tất: {{channels}} kênh, {{add}} để thêm, {{remove}} để xóa, {{fails}} thất bại",
"Batch detection failed": "Phát hiện hàng loạt thất bại", "Batch detection failed": "Phát hiện hàng loạt thất bại",
"Batch disable failed": "Vô hiệu hóa hàng loạt thất bại", "Batch disable failed": "Vô hiệu hóa hàng loạt thất bại",
...@@ -1499,6 +1501,7 @@ ...@@ -1499,6 +1501,7 @@
"Delete (": "Xóa (", "Delete (": "Xóa (",
"Delete {{count}} API key(s)?": "Xóa {{count}} khóa API?", "Delete {{count}} API key(s)?": "Xóa {{count}} khóa API?",
"Delete {{count}} models?": "Xóa {{count}} mô hình?", "Delete {{count}} models?": "Xóa {{count}} mô hình?",
"Delete {{count}} redemption codes?": "Xóa {{count}} mã quy đổi?",
"Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.": "Xóa {{count}} bản ghi nhà cung cấp đã chọn? Không thể xóa nhà cung cấp còn mô hình liên kết.", "Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.": "Xóa {{count}} bản ghi nhà cung cấp đã chọn? Không thể xóa nhà cung cấp còn mô hình liên kết.",
"Delete {{count}} stale instance records? Online instances will not be deleted.": "Xóa {{count}} bản ghi phiên bản mất kết nối? Các phiên bản đang trực tuyến sẽ không bị xóa.", "Delete {{count}} stale instance records? Online instances will not be deleted.": "Xóa {{count}} bản ghi phiên bản mất kết nối? Các phiên bản đang trực tuyến sẽ không bị xóa.",
"Delete a runtime request header": "Xóa header yêu cầu runtime", "Delete a runtime request header": "Xóa header yêu cầu runtime",
...@@ -1540,6 +1543,7 @@ ...@@ -1540,6 +1543,7 @@
"Delete selected API keys": "Xóa các khóa API đã chọn", "Delete selected API keys": "Xóa các khóa API đã chọn",
"Delete selected channels": "Xóa các kênh đã chọn", "Delete selected channels": "Xóa các kênh đã chọn",
"Delete selected models": "Xóa các mô hình đã chọn", "Delete selected models": "Xóa các mô hình đã chọn",
"Delete selected redemption codes": "Xóa mã quy đổi đã chọn",
"Delete stale instance": "Xóa phiên bản mất kết nối", "Delete stale instance": "Xóa phiên bản mất kết nối",
"Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Xóa phiên bản mất kết nối \"{{name}}\"? Nếu phiên bản này đã báo cáo lại, nó sẽ không bị xóa.", "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Xóa phiên bản mất kết nối \"{{name}}\"? Nếu phiên bản này đã báo cáo lại, nó sẽ không bị xóa.",
"Delete stale instances": "Xóa các phiên bản mất kết nối", "Delete stale instances": "Xóa các phiên bản mất kết nối",
...@@ -2081,6 +2085,7 @@ ...@@ -2081,6 +2085,7 @@
"Failed to adjust quota": "Không thể điều chỉnh hạn mức", "Failed to adjust quota": "Không thể điều chỉnh hạn mức",
"Failed to apply overwrite.": "Không thể áp dụng ghi đè.", "Failed to apply overwrite.": "Không thể áp dụng ghi đè.",
"Failed to apply vendor changes": "Không thể áp dụng thay đổi nhà cung cấp", "Failed to apply vendor changes": "Không thể áp dụng thay đổi nhà cung cấp",
"Failed to batch delete redemption codes": "Không thể xóa hàng loạt mã quy đổi",
"Failed to bind email": "Không thể liên kết email", "Failed to bind email": "Không thể liên kết email",
"Failed to change password": "Không thể thay đổi mật khẩu", "Failed to change password": "Không thể thay đổi mật khẩu",
"Failed to check for updates": "Không thể kiểm tra cập nhật", "Failed to check for updates": "Không thể kiểm tra cập nhật",
...@@ -2102,6 +2107,7 @@ ...@@ -2102,6 +2107,7 @@
"Failed to create redemption code": "Không thể tạo mã đổi thưởng", "Failed to create redemption code": "Không thể tạo mã đổi thưởng",
"Failed to create user": "Tạo người dùng thất bại", "Failed to create user": "Tạo người dùng thất bại",
"Failed to delete {{count}} model(s)": "Không thể xóa {{count}} mô hình", "Failed to delete {{count}} model(s)": "Không thể xóa {{count}} mô hình",
"Failed to delete {{count}} redemption codes": "Không thể xóa {{count}} mã quy đổi",
"Failed to delete account": "Không thể xóa tài khoản", "Failed to delete account": "Không thể xóa tài khoản",
"Failed to delete API key": "Xóa API key thất bại", "Failed to delete API key": "Xóa API key thất bại",
"Failed to delete API keys": "Không thể xóa khóa API", "Failed to delete API keys": "Không thể xóa khóa API",
...@@ -2652,6 +2658,8 @@ ...@@ -2652,6 +2658,8 @@
"Incidents": "Sự cố", "Incidents": "Sự cố",
"Include Group": "Bao gồm nhóm", "Include Group": "Bao gồm nhóm",
"Include Model": "Bao gồm mô hình", "Include Model": "Bao gồm mô hình",
"Include name": "Bao gồm tên",
"Include quota": "Bao gồm hạn mức",
"Include Rule Name": "Bao gồm tên quy tắc", "Include Rule Name": "Bao gồm tên quy tắc",
"Includes request rules": "Bao gồm quy tắc yêu cầu", "Includes request rules": "Bao gồm quy tắc yêu cầu",
"Includes tool-call surcharge": "Bao gồm phụ phí gọi công cụ", "Includes tool-call surcharge": "Bao gồm phụ phí gọi công cụ",
...@@ -4266,6 +4274,7 @@ ...@@ -4266,6 +4274,7 @@
"Redemption code(s) created successfully": "Mã đổi thưởng đã tạo thành công", "Redemption code(s) created successfully": "Mã đổi thưởng đã tạo thành công",
"Redemption Codes": "Mã đổi thưởng", "Redemption Codes": "Mã đổi thưởng",
"Redemption codes are disabled until the administrator confirms compliance terms.": "Mã đổi thưởng bị tắt cho đến khi quản trị viên xác nhận điều khoản tuân thủ.", "Redemption codes are disabled until the administrator confirms compliance terms.": "Mã đổi thưởng bị tắt cho đến khi quản trị viên xác nhận điều khoản tuân thủ.",
"Redemption codes created": "Đã tạo mã quy đổi",
"redemption codes.": "Mã đổi thưởng", "redemption codes.": "Mã đổi thưởng",
"Redemption failed": "Đổi thưởng thất bại", "Redemption failed": "Đổi thưởng thất bại",
"Redemption successful! Added: {{quota}}": "Đổi mã thành công! Đã thêm: {{quota}}", "Redemption successful! Added: {{quota}}": "Đổi mã thành công! Đã thêm: {{quota}}",
...@@ -4406,6 +4415,7 @@ ...@@ -4406,6 +4415,7 @@
"Requested items": "Số mục yêu cầu", "Requested items": "Số mục yêu cầu",
"Requested quota": "Số tiền yêu cầu", "Requested quota": "Số tiền yêu cầu",
"Requested quota: {{quota}}": "Số tiền yêu cầu: {{quota}}", "Requested quota: {{quota}}": "Số tiền yêu cầu: {{quota}}",
"Requested redemption code IDs": "ID mã quy đổi được yêu cầu xóa",
"Requested token IDs": "ID mã thông báo được yêu cầu", "Requested token IDs": "ID mã thông báo được yêu cầu",
"Requested token IDs truncated": "Danh sách ID mã thông báo yêu cầu đã được cắt ngắn", "Requested token IDs truncated": "Danh sách ID mã thông báo yêu cầu đã được cắt ngắn",
"Requested: {{total}}": "Yêu cầu: {{total}}", "Requested: {{total}}": "Yêu cầu: {{total}}",
...@@ -4589,6 +4599,9 @@ ...@@ -4589,6 +4599,9 @@
"Save": "Lưu", "Save": "Lưu",
"Save & Submit": "Lưu và gửi", "Save & Submit": "Lưu và gửi",
"Save all settings": "Lưu tất cả cài đặt", "Save all settings": "Lưu tất cả cài đặt",
"Save as a file": "Lưu thành tệp",
"Save as Markdown": "Lưu dạng Markdown",
"Save as TXT": "Lưu dạng TXT",
"Save Backup Codes": "Lưu mã dự phòng", "Save Backup Codes": "Lưu mã dự phòng",
"Save changes": "Lưu thay đổi", "Save changes": "Lưu thay đổi",
"Save Changes": "Lưu Thay đổi", "Save Changes": "Lưu Thay đổi",
...@@ -4614,6 +4627,7 @@ ...@@ -4614,6 +4627,7 @@
"Save Preferences": "Lưu tùy chọn", "Save Preferences": "Lưu tùy chọn",
"Save preview": "Xem trước lưu", "Save preview": "Xem trước lưu",
"Save rate limits": "Lưu giới hạn tốc độ", "Save rate limits": "Lưu giới hạn tốc độ",
"Save redemption codes": "Lưu mã quy đổi",
"Save sensitive words": "Lưu từ nhạy cảm", "Save sensitive words": "Lưu từ nhạy cảm",
"Save Settings": "Lưu Cài đặt", "Save Settings": "Lưu Cài đặt",
"Save sidebar modules": "Lưu các mô-đun thanh bên", "Save sidebar modules": "Lưu các mô-đun thanh bên",
...@@ -5056,6 +5070,7 @@ ...@@ -5056,6 +5070,7 @@
"Successfully deleted {{count}} API key(s)": "Đã xóa thành công {{count}} khóa API", "Successfully deleted {{count}} API key(s)": "Đã xóa thành công {{count}} khóa API",
"Successfully deleted {{count}} invalid redemption codes": "Đã xóa thành công {{count}} mã đổi thưởng không hợp lệ", "Successfully deleted {{count}} invalid redemption codes": "Đã xóa thành công {{count}} mã đổi thưởng không hợp lệ",
"Successfully deleted {{count}} model(s)": "Đã xóa thành công {{count}} mô hình", "Successfully deleted {{count}} model(s)": "Đã xóa thành công {{count}} mô hình",
"Successfully deleted {{count}} redemption codes": "Đã xóa {{count}} mã quy đổi",
"Successfully disabled {{count}} model(s)": "Đã tắt thành công {{count}} mô hình", "Successfully disabled {{count}} model(s)": "Đã tắt thành công {{count}} mô hình",
"Successfully enabled {{count}} model(s)": "Đã bật thành công {{count}} mô hình", "Successfully enabled {{count}} model(s)": "Đã bật thành công {{count}} mô hình",
"Suffix": "Hậu tố", "Suffix": "Hậu tố",
......
...@@ -714,6 +714,8 @@ ...@@ -714,6 +714,8 @@
"Batch delete API tokens": "批次刪除 API 權杖", "Batch delete API tokens": "批次刪除 API 權杖",
"Batch delete failed": "大量刪除失敗", "Batch delete failed": "大量刪除失敗",
"Batch deleted {{count}} channels": "大量刪除 {{count}} 個渠道", "Batch deleted {{count}} channels": "大量刪除 {{count}} 個渠道",
"Batch deleted {{count}} redemption codes": "批次刪除了 {{count}} 個兌換碼",
"Batch deleted redemption codes (count not recorded)": "批次刪除兌換碼(數量未記錄)",
"Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "大量檢測完成:渠道 {{channels}} 個,新增 {{add}} 個,刪除 {{remove}} 個,失敗 {{fails}} 個", "Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "大量檢測完成:渠道 {{channels}} 個,新增 {{add}} 個,刪除 {{remove}} 個,失敗 {{fails}} 個",
"Batch detection failed": "大量檢測失敗", "Batch detection failed": "大量檢測失敗",
"Batch disable failed": "大量停用失敗", "Batch disable failed": "大量停用失敗",
...@@ -1499,6 +1501,7 @@ ...@@ -1499,6 +1501,7 @@
"Delete (": "刪除 (", "Delete (": "刪除 (",
"Delete {{count}} API key(s)?": "刪除 {{count}} 個 API 金鑰?", "Delete {{count}} API key(s)?": "刪除 {{count}} 個 API 金鑰?",
"Delete {{count}} models?": "刪除 {{count}} 個模型?", "Delete {{count}} models?": "刪除 {{count}} 個模型?",
"Delete {{count}} redemption codes?": "刪除 {{count}} 個兌換碼?",
"Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.": "刪除選取的 {{count}} 筆供應商記錄?有關聯模型的供應商無法刪除。", "Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.": "刪除選取的 {{count}} 筆供應商記錄?有關聯模型的供應商無法刪除。",
"Delete {{count}} stale instance records? Online instances will not be deleted.": "刪除 {{count}} 筆失聯實例記錄?線上實例不會被刪除。", "Delete {{count}} stale instance records? Online instances will not be deleted.": "刪除 {{count}} 筆失聯實例記錄?線上實例不會被刪除。",
"Delete a runtime request header": "刪除運行期請求頭", "Delete a runtime request header": "刪除運行期請求頭",
...@@ -1540,6 +1543,7 @@ ...@@ -1540,6 +1543,7 @@
"Delete selected API keys": "刪除選定的 API 金鑰", "Delete selected API keys": "刪除選定的 API 金鑰",
"Delete selected channels": "刪除所選渠道", "Delete selected channels": "刪除所選渠道",
"Delete selected models": "刪除選定的模型", "Delete selected models": "刪除選定的模型",
"Delete selected redemption codes": "刪除選取的兌換碼",
"Delete stale instance": "刪除失聯實例", "Delete stale instance": "刪除失聯實例",
"Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "刪除失聯實例「{{name}}」?如果它已重新上報,將不會被刪除。", "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "刪除失聯實例「{{name}}」?如果它已重新上報,將不會被刪除。",
"Delete stale instances": "刪除失聯實例", "Delete stale instances": "刪除失聯實例",
...@@ -2081,6 +2085,7 @@ ...@@ -2081,6 +2085,7 @@
"Failed to adjust quota": "調整額度失敗", "Failed to adjust quota": "調整額度失敗",
"Failed to apply overwrite.": "套用覆蓋失敗。", "Failed to apply overwrite.": "套用覆蓋失敗。",
"Failed to apply vendor changes": "套用供應商變更失敗", "Failed to apply vendor changes": "套用供應商變更失敗",
"Failed to batch delete redemption codes": "批次刪除兌換碼失敗",
"Failed to bind email": "連結電郵失敗", "Failed to bind email": "連結電郵失敗",
"Failed to change password": "修改密碼失敗", "Failed to change password": "修改密碼失敗",
"Failed to check for updates": "檢查更新失敗", "Failed to check for updates": "檢查更新失敗",
...@@ -2102,6 +2107,7 @@ ...@@ -2102,6 +2107,7 @@
"Failed to create redemption code": "建立兌換碼失敗", "Failed to create redemption code": "建立兌換碼失敗",
"Failed to create user": "建立用戶失敗", "Failed to create user": "建立用戶失敗",
"Failed to delete {{count}} model(s)": "刪除 {{count}} 個模型失敗", "Failed to delete {{count}} model(s)": "刪除 {{count}} 個模型失敗",
"Failed to delete {{count}} redemption codes": "{{count}} 個兌換碼刪除失敗",
"Failed to delete account": "刪除賬號失敗", "Failed to delete account": "刪除賬號失敗",
"Failed to delete API key": "刪除API金鑰失敗", "Failed to delete API key": "刪除API金鑰失敗",
"Failed to delete API keys": "刪除API金鑰失敗", "Failed to delete API keys": "刪除API金鑰失敗",
...@@ -2652,6 +2658,8 @@ ...@@ -2652,6 +2658,8 @@
"Incidents": "事件", "Incidents": "事件",
"Include Group": "包含分組", "Include Group": "包含分組",
"Include Model": "包含模型", "Include Model": "包含模型",
"Include name": "包含名稱",
"Include quota": "包含額度",
"Include Rule Name": "包含規則名", "Include Rule Name": "包含規則名",
"Includes request rules": "包含請求規則", "Includes request rules": "包含請求規則",
"Includes tool-call surcharge": "包含工具呼叫附加費", "Includes tool-call surcharge": "包含工具呼叫附加費",
...@@ -4266,6 +4274,7 @@ ...@@ -4266,6 +4274,7 @@
"Redemption code(s) created successfully": "兌換碼建立成功", "Redemption code(s) created successfully": "兌換碼建立成功",
"Redemption Codes": "兌換碼", "Redemption Codes": "兌換碼",
"Redemption codes are disabled until the administrator confirms compliance terms.": "管理員確認合規條款之前,兌換碼功能不可用。", "Redemption codes are disabled until the administrator confirms compliance terms.": "管理員確認合規條款之前,兌換碼功能不可用。",
"Redemption codes created": "兌換碼建立完成",
"redemption codes.": "兌換碼。", "redemption codes.": "兌換碼。",
"Redemption failed": "兌換失敗", "Redemption failed": "兌換失敗",
"Redemption successful! Added: {{quota}}": "兌換成功!已新增:{{quota}}", "Redemption successful! Added: {{quota}}": "兌換成功!已新增:{{quota}}",
...@@ -4406,6 +4415,7 @@ ...@@ -4406,6 +4415,7 @@
"Requested items": "請求項數", "Requested items": "請求項數",
"Requested quota": "請求金額", "Requested quota": "請求金額",
"Requested quota: {{quota}}": "請求金額:{{quota}}", "Requested quota: {{quota}}": "請求金額:{{quota}}",
"Requested redemption code IDs": "請求刪除的兌換碼 ID",
"Requested token IDs": "請求的權杖 ID", "Requested token IDs": "請求的權杖 ID",
"Requested token IDs truncated": "請求的權杖 ID 已截斷", "Requested token IDs truncated": "請求的權杖 ID 已截斷",
"Requested: {{total}}": "請求 {{total}} 項", "Requested: {{total}}": "請求 {{total}} 項",
...@@ -4589,6 +4599,9 @@ ...@@ -4589,6 +4599,9 @@
"Save": "儲存", "Save": "儲存",
"Save & Submit": "儲存並提交", "Save & Submit": "儲存並提交",
"Save all settings": "儲存所有設定", "Save all settings": "儲存所有設定",
"Save as a file": "儲存為檔案",
"Save as Markdown": "儲存為 Markdown",
"Save as TXT": "儲存為 TXT",
"Save Backup Codes": "儲存備份代碼", "Save Backup Codes": "儲存備份代碼",
"Save changes": "儲存變更", "Save changes": "儲存變更",
"Save Changes": "儲存變更", "Save Changes": "儲存變更",
...@@ -4614,6 +4627,7 @@ ...@@ -4614,6 +4627,7 @@
"Save Preferences": "儲存偏好設定", "Save Preferences": "儲存偏好設定",
"Save preview": "儲存預覽", "Save preview": "儲存預覽",
"Save rate limits": "儲存速率限制", "Save rate limits": "儲存速率限制",
"Save redemption codes": "儲存兌換碼",
"Save sensitive words": "儲存敏感詞", "Save sensitive words": "儲存敏感詞",
"Save Settings": "儲存設定", "Save Settings": "儲存設定",
"Save sidebar modules": "儲存側邊欄模組", "Save sidebar modules": "儲存側邊欄模組",
...@@ -5056,6 +5070,7 @@ ...@@ -5056,6 +5070,7 @@
"Successfully deleted {{count}} API key(s)": "成功刪除了 {{count}} 個 API 金鑰", "Successfully deleted {{count}} API key(s)": "成功刪除了 {{count}} 個 API 金鑰",
"Successfully deleted {{count}} invalid redemption codes": "已成功刪除 {{count}} 個無效兌換碼", "Successfully deleted {{count}} invalid redemption codes": "已成功刪除 {{count}} 個無效兌換碼",
"Successfully deleted {{count}} model(s)": "成功刪除 {{count}} 個模型", "Successfully deleted {{count}} model(s)": "成功刪除 {{count}} 個模型",
"Successfully deleted {{count}} redemption codes": "成功刪除 {{count}} 個兌換碼",
"Successfully disabled {{count}} model(s)": "成功停用 {{count}} 個模型", "Successfully disabled {{count}} model(s)": "成功停用 {{count}} 個模型",
"Successfully enabled {{count}} model(s)": "成功啟用 {{count}} 個模型", "Successfully enabled {{count}} model(s)": "成功啟用 {{count}} 個模型",
"Suffix": "後綴", "Suffix": "後綴",
......
...@@ -714,6 +714,8 @@ ...@@ -714,6 +714,8 @@
"Batch delete API tokens": "批量删除 API 令牌", "Batch delete API tokens": "批量删除 API 令牌",
"Batch delete failed": "批量删除失败", "Batch delete failed": "批量删除失败",
"Batch deleted {{count}} channels": "批量删除 {{count}} 个渠道", "Batch deleted {{count}} channels": "批量删除 {{count}} 个渠道",
"Batch deleted {{count}} redemption codes": "批量删除了 {{count}} 个兑换码",
"Batch deleted redemption codes (count not recorded)": "批量删除兑换码(数量未记录)",
"Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "批量检测完成:渠道 {{channels}} 个,新增 {{add}} 个,删除 {{remove}} 个,失败 {{fails}} 个", "Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed": "批量检测完成:渠道 {{channels}} 个,新增 {{add}} 个,删除 {{remove}} 个,失败 {{fails}} 个",
"Batch detection failed": "批量检测失败", "Batch detection failed": "批量检测失败",
"Batch disable failed": "批量禁用失败", "Batch disable failed": "批量禁用失败",
...@@ -1499,6 +1501,7 @@ ...@@ -1499,6 +1501,7 @@
"Delete (": "删除 (", "Delete (": "删除 (",
"Delete {{count}} API key(s)?": "删除 {{count}} 个 API 密钥?", "Delete {{count}} API key(s)?": "删除 {{count}} 个 API 密钥?",
"Delete {{count}} models?": "删除 {{count}} 个模型?", "Delete {{count}} models?": "删除 {{count}} 个模型?",
"Delete {{count}} redemption codes?": "删除 {{count}} 个兑换码?",
"Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.": "删除选中的 {{count}} 条供应商记录?存在关联模型的供应商无法删除。", "Delete {{count}} selected vendor records? Vendors with linked models cannot be deleted.": "删除选中的 {{count}} 条供应商记录?存在关联模型的供应商无法删除。",
"Delete {{count}} stale instance records? Online instances will not be deleted.": "删除 {{count}} 条失联实例记录?在线实例不会被删除。", "Delete {{count}} stale instance records? Online instances will not be deleted.": "删除 {{count}} 条失联实例记录?在线实例不会被删除。",
"Delete a runtime request header": "删除运行期请求头", "Delete a runtime request header": "删除运行期请求头",
...@@ -1540,6 +1543,7 @@ ...@@ -1540,6 +1543,7 @@
"Delete selected API keys": "删除选定的 API 密钥", "Delete selected API keys": "删除选定的 API 密钥",
"Delete selected channels": "删除所选渠道", "Delete selected channels": "删除所选渠道",
"Delete selected models": "删除选定的模型", "Delete selected models": "删除选定的模型",
"Delete selected redemption codes": "删除选中的兑换码",
"Delete stale instance": "删除失联实例", "Delete stale instance": "删除失联实例",
"Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "删除失联实例 \"{{name}}\"?如果它已经重新上报,将不会被删除。", "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "删除失联实例 \"{{name}}\"?如果它已经重新上报,将不会被删除。",
"Delete stale instances": "删除失联实例", "Delete stale instances": "删除失联实例",
...@@ -2081,6 +2085,7 @@ ...@@ -2081,6 +2085,7 @@
"Failed to adjust quota": "调整额度失败", "Failed to adjust quota": "调整额度失败",
"Failed to apply overwrite.": "应用覆盖失败。", "Failed to apply overwrite.": "应用覆盖失败。",
"Failed to apply vendor changes": "应用供应商更改失败", "Failed to apply vendor changes": "应用供应商更改失败",
"Failed to batch delete redemption codes": "批量删除兑换码失败",
"Failed to bind email": "绑定邮箱失败", "Failed to bind email": "绑定邮箱失败",
"Failed to change password": "修改密码失败", "Failed to change password": "修改密码失败",
"Failed to check for updates": "检查更新失败", "Failed to check for updates": "检查更新失败",
...@@ -2102,6 +2107,7 @@ ...@@ -2102,6 +2107,7 @@
"Failed to create redemption code": "创建兑换码失败", "Failed to create redemption code": "创建兑换码失败",
"Failed to create user": "创建用户失败", "Failed to create user": "创建用户失败",
"Failed to delete {{count}} model(s)": "删除 {{count}} 个模型失败", "Failed to delete {{count}} model(s)": "删除 {{count}} 个模型失败",
"Failed to delete {{count}} redemption codes": "{{count}} 个兑换码删除失败",
"Failed to delete account": "删除账号失败", "Failed to delete account": "删除账号失败",
"Failed to delete API key": "删除API密钥失败", "Failed to delete API key": "删除API密钥失败",
"Failed to delete API keys": "删除API密钥失败", "Failed to delete API keys": "删除API密钥失败",
...@@ -2652,6 +2658,8 @@ ...@@ -2652,6 +2658,8 @@
"Incidents": "事件", "Incidents": "事件",
"Include Group": "包含分组", "Include Group": "包含分组",
"Include Model": "包含模型", "Include Model": "包含模型",
"Include name": "包含名称",
"Include quota": "包含额度",
"Include Rule Name": "包含规则名", "Include Rule Name": "包含规则名",
"Includes request rules": "包含请求规则", "Includes request rules": "包含请求规则",
"Includes tool-call surcharge": "包含工具调用附加费", "Includes tool-call surcharge": "包含工具调用附加费",
...@@ -4266,6 +4274,7 @@ ...@@ -4266,6 +4274,7 @@
"Redemption code(s) created successfully": "兑换码创建成功", "Redemption code(s) created successfully": "兑换码创建成功",
"Redemption Codes": "兑换码", "Redemption Codes": "兑换码",
"Redemption codes are disabled until the administrator confirms compliance terms.": "管理员确认合规条款之前,兑换码功能不可用。", "Redemption codes are disabled until the administrator confirms compliance terms.": "管理员确认合规条款之前,兑换码功能不可用。",
"Redemption codes created": "兑换码创建完成",
"redemption codes.": "兑换码。", "redemption codes.": "兑换码。",
"Redemption failed": "兑换失败", "Redemption failed": "兑换失败",
"Redemption successful! Added: {{quota}}": "兑换成功!已添加:{{quota}}", "Redemption successful! Added: {{quota}}": "兑换成功!已添加:{{quota}}",
...@@ -4406,6 +4415,7 @@ ...@@ -4406,6 +4415,7 @@
"Requested items": "请求项数", "Requested items": "请求项数",
"Requested quota": "请求数额", "Requested quota": "请求数额",
"Requested quota: {{quota}}": "请求数额:{{quota}}", "Requested quota: {{quota}}": "请求数额:{{quota}}",
"Requested redemption code IDs": "请求删除的兑换码 ID",
"Requested token IDs": "请求的令牌 ID", "Requested token IDs": "请求的令牌 ID",
"Requested token IDs truncated": "请求的令牌 ID 已截断", "Requested token IDs truncated": "请求的令牌 ID 已截断",
"Requested: {{total}}": "请求 {{total}} 项", "Requested: {{total}}": "请求 {{total}} 项",
...@@ -4589,6 +4599,9 @@ ...@@ -4589,6 +4599,9 @@
"Save": "保存", "Save": "保存",
"Save & Submit": "保存并提交", "Save & Submit": "保存并提交",
"Save all settings": "保存所有设置", "Save all settings": "保存所有设置",
"Save as a file": "保存为文件",
"Save as Markdown": "保存为 Markdown",
"Save as TXT": "保存为 TXT",
"Save Backup Codes": "保存备份代码", "Save Backup Codes": "保存备份代码",
"Save changes": "保存更改", "Save changes": "保存更改",
"Save Changes": "保存更改", "Save Changes": "保存更改",
...@@ -4614,6 +4627,7 @@ ...@@ -4614,6 +4627,7 @@
"Save Preferences": "保存偏好设置", "Save Preferences": "保存偏好设置",
"Save preview": "保存预览", "Save preview": "保存预览",
"Save rate limits": "保存速率限制", "Save rate limits": "保存速率限制",
"Save redemption codes": "保存兑换码",
"Save sensitive words": "保存敏感词", "Save sensitive words": "保存敏感词",
"Save Settings": "保存设置", "Save Settings": "保存设置",
"Save sidebar modules": "保存侧边栏模块", "Save sidebar modules": "保存侧边栏模块",
...@@ -5056,6 +5070,7 @@ ...@@ -5056,6 +5070,7 @@
"Successfully deleted {{count}} API key(s)": "成功删除了 {{count}} 个 API 密钥", "Successfully deleted {{count}} API key(s)": "成功删除了 {{count}} 个 API 密钥",
"Successfully deleted {{count}} invalid redemption codes": "已成功删除 {{count}} 个无效兑换码", "Successfully deleted {{count}} invalid redemption codes": "已成功删除 {{count}} 个无效兑换码",
"Successfully deleted {{count}} model(s)": "成功删除 {{count}} 个模型", "Successfully deleted {{count}} model(s)": "成功删除 {{count}} 个模型",
"Successfully deleted {{count}} redemption codes": "成功删除 {{count}} 个兑换码",
"Successfully disabled {{count}} model(s)": "成功禁用 {{count}} 个模型", "Successfully disabled {{count}} model(s)": "成功禁用 {{count}} 个模型",
"Successfully enabled {{count}} model(s)": "成功启用 {{count}} 个模型", "Successfully enabled {{count}} model(s)": "成功启用 {{count}} 个模型",
"Suffix": "后缀", "Suffix": "后缀",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment