feat(json-editor): add reusable JSON code editor
- introduce a shared themed JSON editor with line numbers, formatting, status feedback, and keyboard editing helpers. - use the shared editor in model pricing JSON mode so pricing maps get consistent editor behavior. - localize structured JSON validation messages so parse errors avoid browser-specific English text.
This commit is contained in:
+284
@@ -0,0 +1,284 @@
|
|||||||
|
/*
|
||||||
|
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 {
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type ComponentProps,
|
||||||
|
type KeyboardEvent,
|
||||||
|
} from 'react'
|
||||||
|
import { AlertCircle, Braces, CheckCircle2, Code2 } from 'lucide-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
|
||||||
|
export type JsonCodeEditorProps = Omit<ComponentProps<'div'>, 'onChange'> & {
|
||||||
|
value: string
|
||||||
|
onChange: (value: string) => void
|
||||||
|
disabled?: boolean
|
||||||
|
heightClassName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JsonCodeEditor({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
heightClassName = 'h-56 min-h-56 max-h-56',
|
||||||
|
className,
|
||||||
|
id,
|
||||||
|
'aria-describedby': ariaDescribedBy,
|
||||||
|
'aria-invalid': ariaInvalid,
|
||||||
|
...rootProps
|
||||||
|
}: JsonCodeEditorProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
const [scrollTop, setScrollTop] = useState(0)
|
||||||
|
const lineNumbers = useMemo(() => {
|
||||||
|
const count = Math.max(1, value.split('\n').length)
|
||||||
|
return Array.from({ length: count }, (_, index) => index + 1)
|
||||||
|
}, [value])
|
||||||
|
const jsonStatus = useMemo(() => {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) return { valid: true, message: t('JSON') }
|
||||||
|
try {
|
||||||
|
JSON.parse(trimmed)
|
||||||
|
return { valid: true, message: t('JSON') }
|
||||||
|
} catch {
|
||||||
|
return { valid: false, message: t('Invalid JSON') }
|
||||||
|
}
|
||||||
|
}, [value, t])
|
||||||
|
|
||||||
|
const formatJson = () => {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) return
|
||||||
|
try {
|
||||||
|
onChange(JSON.stringify(JSON.parse(trimmed), null, 2))
|
||||||
|
} catch {
|
||||||
|
// Keep invalid drafts untouched; validation feedback remains visible.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateValueWithSelection = (
|
||||||
|
nextValue: string,
|
||||||
|
selectionStart: number,
|
||||||
|
selectionEnd = selectionStart
|
||||||
|
) => {
|
||||||
|
onChange(nextValue)
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
textareaRef.current?.setSelectionRange(selectionStart, selectionEnd)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLineIndent = (text: string, cursor: number) => {
|
||||||
|
const lineStart = text.lastIndexOf('\n', cursor - 1) + 1
|
||||||
|
return text.slice(lineStart, cursor).match(/^\s*/)?.[0] ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditorKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
const target = event.currentTarget
|
||||||
|
const start = target.selectionStart
|
||||||
|
const end = target.selectionEnd
|
||||||
|
const selected = value.slice(start, end)
|
||||||
|
const before = value.slice(0, start)
|
||||||
|
const after = value.slice(end)
|
||||||
|
|
||||||
|
if (event.key === 'Tab') {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (start !== end && selected.includes('\n')) {
|
||||||
|
const selectionLineStart = value.lastIndexOf('\n', start - 1) + 1
|
||||||
|
const selectedBlock = value.slice(selectionLineStart, end)
|
||||||
|
const lines = selectedBlock.split('\n')
|
||||||
|
const nextBlock = event.shiftKey
|
||||||
|
? lines
|
||||||
|
.map((line) =>
|
||||||
|
line.startsWith(' ')
|
||||||
|
? line.slice(2)
|
||||||
|
: line.startsWith('\t')
|
||||||
|
? line.slice(1)
|
||||||
|
: line
|
||||||
|
)
|
||||||
|
.join('\n')
|
||||||
|
: lines.map((line) => ` ${line}`).join('\n')
|
||||||
|
const nextValue =
|
||||||
|
value.slice(0, selectionLineStart) + nextBlock + value.slice(end)
|
||||||
|
updateValueWithSelection(
|
||||||
|
nextValue,
|
||||||
|
selectionLineStart,
|
||||||
|
selectionLineStart + nextBlock.length
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.shiftKey) {
|
||||||
|
const lineStart = value.lastIndexOf('\n', start - 1) + 1
|
||||||
|
const removable = value.slice(lineStart, lineStart + 2)
|
||||||
|
if (removable === ' ') {
|
||||||
|
updateValueWithSelection(
|
||||||
|
value.slice(0, lineStart) + value.slice(lineStart + 2),
|
||||||
|
Math.max(lineStart, start - 2),
|
||||||
|
Math.max(lineStart, end - 2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateValueWithSelection(`${before} ${after}`, start + 2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault()
|
||||||
|
const indent = getLineIndent(value, start)
|
||||||
|
const previousChar = before.trimEnd().at(-1)
|
||||||
|
const nextChar = after.trimStart().at(0)
|
||||||
|
const shouldNest = previousChar === '{' || previousChar === '['
|
||||||
|
const shouldClose =
|
||||||
|
(previousChar === '{' && nextChar === '}') ||
|
||||||
|
(previousChar === '[' && nextChar === ']')
|
||||||
|
|
||||||
|
if (shouldNest && shouldClose) {
|
||||||
|
const innerIndent = `${indent} `
|
||||||
|
const insert = `\n${innerIndent}\n${indent}`
|
||||||
|
updateValueWithSelection(
|
||||||
|
`${before}${insert}${after}`,
|
||||||
|
start + 1 + innerIndent.length
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextIndent = shouldNest ? `${indent} ` : indent
|
||||||
|
const insert = `\n${nextIndent}`
|
||||||
|
updateValueWithSelection(
|
||||||
|
`${before}${insert}${after}`,
|
||||||
|
start + insert.length
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pairs: Record<string, string> = {
|
||||||
|
'"': '"',
|
||||||
|
'{': '}',
|
||||||
|
'[': ']',
|
||||||
|
}
|
||||||
|
const closingChars = new Set(Object.values(pairs))
|
||||||
|
|
||||||
|
if (closingChars.has(event.key) && value[start] === event.key) {
|
||||||
|
event.preventDefault()
|
||||||
|
textareaRef.current?.setSelectionRange(start + 1, start + 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pairs[event.key]) {
|
||||||
|
event.preventDefault()
|
||||||
|
const close = pairs[event.key]
|
||||||
|
const wrapped = `${event.key}${selected}${close}`
|
||||||
|
updateValueWithSelection(
|
||||||
|
`${before}${wrapped}${after}`,
|
||||||
|
start + 1,
|
||||||
|
start + 1 + selected.length
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Backspace' && start === end && start > 0) {
|
||||||
|
const previousChar = value[start - 1]
|
||||||
|
const nextChar = value[start]
|
||||||
|
if (pairs[previousChar] === nextChar) {
|
||||||
|
event.preventDefault()
|
||||||
|
updateValueWithSelection(
|
||||||
|
value.slice(0, start - 1) + value.slice(start + 1),
|
||||||
|
start - 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'border-input bg-background focus-within:border-ring focus-within:ring-ring/50 overflow-hidden rounded-lg border transition-colors focus-within:ring-3',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...rootProps}
|
||||||
|
>
|
||||||
|
<div className='bg-muted/30 flex h-8 items-center justify-between border-b px-2'>
|
||||||
|
<div className='text-muted-foreground flex min-w-0 items-center gap-1.5 text-xs font-medium'>
|
||||||
|
<Braces className='h-3.5 w-3.5' />
|
||||||
|
<span>{t('JSON')}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-1 text-xs',
|
||||||
|
jsonStatus.valid ? 'text-emerald-600' : 'text-destructive'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{jsonStatus.valid ? (
|
||||||
|
<CheckCircle2 className='h-3.5 w-3.5' />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className='h-3.5 w-3.5' />
|
||||||
|
)}
|
||||||
|
{jsonStatus.message}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
size='sm'
|
||||||
|
className='h-6 px-2 text-xs'
|
||||||
|
onClick={formatJson}
|
||||||
|
disabled={disabled || !jsonStatus.valid || !value.trim()}
|
||||||
|
>
|
||||||
|
<Code2 className='mr-1 h-3.5 w-3.5' />
|
||||||
|
{t('Format JSON')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={cn('relative flex overflow-hidden', heightClassName)}>
|
||||||
|
<div className='bg-muted/20 text-muted-foreground/70 relative w-10 shrink-0 overflow-hidden border-r font-mono text-xs leading-5 select-none'>
|
||||||
|
<div
|
||||||
|
className='px-2 py-2 text-right'
|
||||||
|
style={{ transform: `translateY(-${scrollTop}px)` }}
|
||||||
|
>
|
||||||
|
{lineNumbers.map((lineNumber) => (
|
||||||
|
<div key={lineNumber}>{lineNumber}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
id={id}
|
||||||
|
aria-describedby={ariaDescribedBy}
|
||||||
|
aria-invalid={ariaInvalid}
|
||||||
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
onKeyDown={handleEditorKeyDown}
|
||||||
|
onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}
|
||||||
|
className={cn(
|
||||||
|
'[field-sizing:fixed] resize-none overflow-auto rounded-none border-0 bg-transparent px-3 py-2 font-mono text-xs leading-5 shadow-none ring-0 outline-none focus-visible:ring-0',
|
||||||
|
heightClassName
|
||||||
|
)}
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { JsonCodeEditor } from '@/components/json-code-editor'
|
||||||
import {
|
import {
|
||||||
SettingsForm,
|
SettingsForm,
|
||||||
SettingsSwitchContent,
|
SettingsSwitchContent,
|
||||||
@@ -140,10 +140,9 @@ function ModelJsonTextareaField(props: {
|
|||||||
<FormItem className='flex min-w-0 flex-col gap-2'>
|
<FormItem className='flex min-w-0 flex-col gap-2'>
|
||||||
<FormLabel>{props.label}</FormLabel>
|
<FormLabel>{props.label}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<JsonCodeEditor
|
||||||
{...field}
|
value={field.value}
|
||||||
className='h-56 min-h-56 max-h-56 resize-none overflow-auto [field-sizing:fixed] font-mono text-xs leading-5'
|
onChange={(value) => field.onChange(value)}
|
||||||
spellCheck={false}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription className='text-xs leading-5'>
|
<FormDescription className='text-xs leading-5'>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ 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 { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
@@ -34,169 +34,99 @@ import { ToolPriceSettings } from './tool-price-settings'
|
|||||||
import { UpstreamRatioSync } from './upstream-ratio-sync'
|
import { UpstreamRatioSync } from './upstream-ratio-sync'
|
||||||
import {
|
import {
|
||||||
formatJsonForTextarea,
|
formatJsonForTextarea,
|
||||||
|
type JsonValidationError,
|
||||||
normalizeJsonString,
|
normalizeJsonString,
|
||||||
validateJsonString,
|
validateJsonString,
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
|
||||||
const modelSchema = z.object({
|
type Translate = (key: string, options?: Record<string, unknown>) => string
|
||||||
ModelPrice: z.string().superRefine((value, ctx) => {
|
|
||||||
const result = validateJsonString(value)
|
function formatJsonValidationError(
|
||||||
|
t: Translate,
|
||||||
|
error?: JsonValidationError,
|
||||||
|
fallback = 'Invalid JSON'
|
||||||
|
) {
|
||||||
|
if (!error) return t(fallback)
|
||||||
|
|
||||||
|
if (error.type === 'required') return t('Value is required')
|
||||||
|
if (error.type === 'structure') {
|
||||||
|
return t(
|
||||||
|
fallback === 'Invalid JSON' ? 'JSON structure is invalid' : fallback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = [
|
||||||
|
error.line && error.column
|
||||||
|
? t('JSON is invalid at line {{line}}, column {{column}}.', {
|
||||||
|
line: error.line,
|
||||||
|
column: error.column,
|
||||||
|
})
|
||||||
|
: error.position !== undefined
|
||||||
|
? t('JSON is invalid at position {{position}}.', {
|
||||||
|
position: error.position,
|
||||||
|
})
|
||||||
|
: t('JSON is invalid. Please check the syntax.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (error.missingCommaLine) {
|
||||||
|
parts.push(
|
||||||
|
t('Check line {{line}} for a missing comma.', {
|
||||||
|
line: error.missingCommaLine,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
function createJsonStringField(
|
||||||
|
t: Translate,
|
||||||
|
options?: Parameters<typeof validateJsonString>[1]
|
||||||
|
) {
|
||||||
|
return z.string().superRefine((value, ctx) => {
|
||||||
|
const result = validateJsonString(value, options)
|
||||||
if (!result.valid) {
|
if (!result.valid) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: result.message || 'Invalid JSON',
|
message: formatJsonValidationError(t, result.error, result.message),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
ModelRatio: z.string().superRefine((value, ctx) => {
|
|
||||||
const result = validateJsonString(value)
|
|
||||||
if (!result.valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: result.message || 'Invalid JSON',
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
CacheRatio: z.string().superRefine((value, ctx) => {
|
const createModelSchema = (t: Translate) =>
|
||||||
const result = validateJsonString(value)
|
z.object({
|
||||||
if (!result.valid) {
|
ModelPrice: createJsonStringField(t),
|
||||||
ctx.addIssue({
|
ModelRatio: createJsonStringField(t),
|
||||||
code: z.ZodIssueCode.custom,
|
CacheRatio: createJsonStringField(t),
|
||||||
message: result.message || 'Invalid JSON',
|
CreateCacheRatio: createJsonStringField(t),
|
||||||
})
|
CompletionRatio: createJsonStringField(t),
|
||||||
}
|
ImageRatio: createJsonStringField(t),
|
||||||
}),
|
AudioRatio: createJsonStringField(t),
|
||||||
CreateCacheRatio: z.string().superRefine((value, ctx) => {
|
AudioCompletionRatio: createJsonStringField(t),
|
||||||
const result = validateJsonString(value)
|
|
||||||
if (!result.valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: result.message || 'Invalid JSON',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
CompletionRatio: z.string().superRefine((value, ctx) => {
|
|
||||||
const result = validateJsonString(value)
|
|
||||||
if (!result.valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: result.message || 'Invalid JSON',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
ImageRatio: z.string().superRefine((value, ctx) => {
|
|
||||||
const result = validateJsonString(value)
|
|
||||||
if (!result.valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: result.message || 'Invalid JSON',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
AudioRatio: z.string().superRefine((value, ctx) => {
|
|
||||||
const result = validateJsonString(value)
|
|
||||||
if (!result.valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: result.message || 'Invalid JSON',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
AudioCompletionRatio: z.string().superRefine((value, ctx) => {
|
|
||||||
const result = validateJsonString(value)
|
|
||||||
if (!result.valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: result.message || 'Invalid JSON',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
ExposeRatioEnabled: z.boolean(),
|
ExposeRatioEnabled: z.boolean(),
|
||||||
BillingMode: z.string().superRefine((value, ctx) => {
|
BillingMode: createJsonStringField(t),
|
||||||
const result = validateJsonString(value)
|
BillingExpr: createJsonStringField(t),
|
||||||
if (!result.valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: result.message || 'Invalid JSON',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
BillingExpr: z.string().superRefine((value, ctx) => {
|
|
||||||
const result = validateJsonString(value)
|
|
||||||
if (!result.valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: result.message || 'Invalid JSON',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const groupSchema = z.object({
|
const createGroupSchema = (t: Translate) =>
|
||||||
GroupRatio: z.string().superRefine((value, ctx) => {
|
z.object({
|
||||||
const result = validateJsonString(value)
|
GroupRatio: createJsonStringField(t),
|
||||||
if (!result.valid) {
|
TopupGroupRatio: createJsonStringField(t),
|
||||||
ctx.addIssue({
|
UserUsableGroups: createJsonStringField(t),
|
||||||
code: z.ZodIssueCode.custom,
|
GroupGroupRatio: createJsonStringField(t),
|
||||||
message: result.message || 'Invalid JSON',
|
AutoGroups: createJsonStringField(t, {
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
TopupGroupRatio: z.string().superRefine((value, ctx) => {
|
|
||||||
const result = validateJsonString(value)
|
|
||||||
if (!result.valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: result.message || 'Invalid JSON',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
UserUsableGroups: z.string().superRefine((value, ctx) => {
|
|
||||||
const result = validateJsonString(value)
|
|
||||||
if (!result.valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: result.message || 'Invalid JSON',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
GroupGroupRatio: z.string().superRefine((value, ctx) => {
|
|
||||||
const result = validateJsonString(value)
|
|
||||||
if (!result.valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: result.message || 'Invalid JSON',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
AutoGroups: z.string().superRefine((value, ctx) => {
|
|
||||||
const result = validateJsonString(value, {
|
|
||||||
predicate: (parsed) =>
|
predicate: (parsed) =>
|
||||||
Array.isArray(parsed) &&
|
Array.isArray(parsed) &&
|
||||||
parsed.every((item) => typeof item === 'string'),
|
parsed.every((item) => typeof item === 'string'),
|
||||||
predicateMessage: 'Expected a JSON array of group identifiers',
|
predicateMessage: 'Expected a JSON array of group identifiers',
|
||||||
})
|
|
||||||
if (!result.valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: result.message || 'Invalid JSON array',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
DefaultUseAutoGroup: z.boolean(),
|
DefaultUseAutoGroup: z.boolean(),
|
||||||
GroupSpecialUsableGroup: z.string().superRefine((value, ctx) => {
|
GroupSpecialUsableGroup: createJsonStringField(t),
|
||||||
const result = validateJsonString(value)
|
|
||||||
if (!result.valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: result.message || 'Invalid JSON',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
type ModelFormValues = z.infer<typeof modelSchema>
|
type ModelFormValues = z.infer<ReturnType<typeof createModelSchema>>
|
||||||
type GroupFormValues = z.infer<typeof groupSchema>
|
type GroupFormValues = z.infer<ReturnType<typeof createGroupSchema>>
|
||||||
type RatioTabId = 'models' | 'groups' | 'tool-prices' | 'upstream-sync'
|
type RatioTabId = 'models' | 'groups' | 'tool-prices' | 'upstream-sync'
|
||||||
|
|
||||||
type RatioSettingsCardProps = {
|
type RatioSettingsCardProps = {
|
||||||
@@ -265,6 +195,8 @@ export function RatioSettingsCard({
|
|||||||
groupDefaults.GroupSpecialUsableGroup
|
groupDefaults.GroupSpecialUsableGroup
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
const modelSchema = useMemo(() => createModelSchema(t), [t])
|
||||||
|
const groupSchema = useMemo(() => createGroupSchema(t), [t])
|
||||||
|
|
||||||
const modelForm = useForm<ModelFormValues>({
|
const modelForm = useForm<ModelFormValues>({
|
||||||
resolver: zodResolver(modelSchema),
|
resolver: zodResolver(modelSchema),
|
||||||
|
|||||||
+47
-4
@@ -49,6 +49,14 @@ type JsonValidationOptions = {
|
|||||||
predicateMessage?: string
|
predicateMessage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type JsonValidationError = {
|
||||||
|
type: 'required' | 'structure' | 'syntax'
|
||||||
|
line?: number
|
||||||
|
column?: number
|
||||||
|
position?: number
|
||||||
|
missingCommaLine?: number
|
||||||
|
}
|
||||||
|
|
||||||
function extractErrorPosition(
|
function extractErrorPosition(
|
||||||
error: unknown,
|
error: unknown,
|
||||||
jsonString: string
|
jsonString: string
|
||||||
@@ -81,8 +89,15 @@ function extractErrorPosition(
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatErrorMessage(error: unknown, jsonString: string): string {
|
function buildSyntaxError(
|
||||||
if (!(error instanceof Error)) return 'Invalid JSON'
|
error: unknown,
|
||||||
|
jsonString: string
|
||||||
|
): JsonValidationError {
|
||||||
|
if (!(error instanceof Error)) {
|
||||||
|
return {
|
||||||
|
type: 'syntax',
|
||||||
|
} satisfies JsonValidationError
|
||||||
|
}
|
||||||
|
|
||||||
const position = extractErrorPosition(error, jsonString)
|
const position = extractErrorPosition(error, jsonString)
|
||||||
const message = error.message
|
const message = error.message
|
||||||
@@ -93,10 +108,29 @@ function formatErrorMessage(error: unknown, jsonString: string): string {
|
|||||||
message.includes('Expected property name') ||
|
message.includes('Expected property name') ||
|
||||||
message.includes('Unexpected string')
|
message.includes('Unexpected string')
|
||||||
|
|
||||||
|
const missingCommaLine =
|
||||||
|
isMissingCommaError && position.line && position.line > 1
|
||||||
|
? position.line - 1
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'syntax',
|
||||||
|
...position,
|
||||||
|
missingCommaLine,
|
||||||
|
} satisfies JsonValidationError
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatErrorMessage(error: unknown, jsonString: string): string {
|
||||||
|
if (!(error instanceof Error)) return 'Invalid JSON'
|
||||||
|
|
||||||
|
const position = extractErrorPosition(error, jsonString)
|
||||||
|
const message = error.message
|
||||||
|
const syntaxError = buildSyntaxError(error, jsonString)
|
||||||
|
|
||||||
if (position.line && position.column) {
|
if (position.line && position.column) {
|
||||||
let hint = ''
|
let hint = ''
|
||||||
if (isMissingCommaError && position.line > 1) {
|
if (syntaxError.missingCommaLine) {
|
||||||
hint = ` (check line ${position.line - 1} for missing comma)`
|
hint = ` (check line ${syntaxError.missingCommaLine} for missing comma)`
|
||||||
}
|
}
|
||||||
return `Error at line ${position.line}, column ${position.column}: ${message}${hint}`
|
return `Error at line ${position.line}, column ${position.column}: ${message}${hint}`
|
||||||
}
|
}
|
||||||
@@ -119,6 +153,11 @@ export function validateJsonString(
|
|||||||
return {
|
return {
|
||||||
valid: allowEmpty,
|
valid: allowEmpty,
|
||||||
message: allowEmpty ? undefined : 'Value is required',
|
message: allowEmpty ? undefined : 'Value is required',
|
||||||
|
error: allowEmpty
|
||||||
|
? undefined
|
||||||
|
: ({
|
||||||
|
type: 'required',
|
||||||
|
} satisfies JsonValidationError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +167,9 @@ export function validateJsonString(
|
|||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: predicateMessage || 'JSON structure is invalid',
|
message: predicateMessage || 'JSON structure is invalid',
|
||||||
|
error: {
|
||||||
|
type: 'structure',
|
||||||
|
} satisfies JsonValidationError,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +178,7 @@ export function validateJsonString(
|
|||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: formatErrorMessage(error, trimmed),
|
message: formatErrorMessage(error, trimmed),
|
||||||
|
error: buildSyntaxError(error, trimmed),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+7
@@ -679,6 +679,7 @@
|
|||||||
"Check for updates": "Check for updates",
|
"Check for updates": "Check for updates",
|
||||||
"Check in daily to receive random quota rewards": "Check in daily to receive random quota rewards",
|
"Check in daily to receive random quota rewards": "Check in daily to receive random quota rewards",
|
||||||
"Check in now": "Check in now",
|
"Check in now": "Check in now",
|
||||||
|
"Check line {{line}} for a missing comma.": "Check line {{line}} for a missing comma.",
|
||||||
"Check out the Quick Start": "Check out the Quick Start",
|
"Check out the Quick Start": "Check out the Quick Start",
|
||||||
"Check resolved IPs against IP filters even when accessing by domain": "Check resolved IPs against IP filters even when accessing by domain",
|
"Check resolved IPs against IP filters even when accessing by domain": "Check resolved IPs against IP filters even when accessing by domain",
|
||||||
"Check-in failed": "Check-in failed",
|
"Check-in failed": "Check-in failed",
|
||||||
@@ -1527,6 +1528,7 @@
|
|||||||
"Expand": "Expand",
|
"Expand": "Expand",
|
||||||
"Expand All": "Expand All",
|
"Expand All": "Expand All",
|
||||||
"Expected a JSON array.": "Expected a JSON array.",
|
"Expected a JSON array.": "Expected a JSON array.",
|
||||||
|
"Expected a JSON array of group identifiers": "Expected a JSON array of group identifiers",
|
||||||
"Experiment with prompts and models in real time.": "Experiment with prompts and models in real time.",
|
"Experiment with prompts and models in real time.": "Experiment with prompts and models in real time.",
|
||||||
"Expiration Time": "Expiration Time",
|
"Expiration Time": "Expiration Time",
|
||||||
"expired": "expired",
|
"expired": "expired",
|
||||||
@@ -2093,6 +2095,9 @@
|
|||||||
"JSON Editor": "JSON Editor",
|
"JSON Editor": "JSON Editor",
|
||||||
"JSON format error": "JSON format error",
|
"JSON format error": "JSON format error",
|
||||||
"JSON format supports service account JSON files": "JSON format supports service account JSON files",
|
"JSON format supports service account JSON files": "JSON format supports service account JSON files",
|
||||||
|
"JSON is invalid at line {{line}}, column {{column}}.": "JSON is invalid at line {{line}}, column {{column}}.",
|
||||||
|
"JSON is invalid at position {{position}}.": "JSON is invalid at position {{position}}.",
|
||||||
|
"JSON is invalid. Please check the syntax.": "JSON is invalid. Please check the syntax.",
|
||||||
"JSON map of group → description exposed when users create API keys.": "JSON map of group → description exposed when users create API keys.",
|
"JSON map of group → description exposed when users create API keys.": "JSON map of group → description exposed when users create API keys.",
|
||||||
"JSON map of group → ratio applied when the user selects the group explicitly.": "JSON map of group → ratio applied when the user selects the group explicitly.",
|
"JSON map of group → ratio applied when the user selects the group explicitly.": "JSON map of group → ratio applied when the user selects the group explicitly.",
|
||||||
"JSON map of model → multiplier applied to quota billing.": "JSON map of model → multiplier applied to quota billing.",
|
"JSON map of model → multiplier applied to quota billing.": "JSON map of model → multiplier applied to quota billing.",
|
||||||
@@ -2101,6 +2106,7 @@
|
|||||||
"JSON Mode": "JSON Mode",
|
"JSON Mode": "JSON Mode",
|
||||||
"JSON must be an object": "JSON must be an object",
|
"JSON must be an object": "JSON must be an object",
|
||||||
"JSON object:": "JSON object:",
|
"JSON object:": "JSON object:",
|
||||||
|
"JSON structure is invalid": "JSON structure is invalid",
|
||||||
"JSON Text": "JSON Text",
|
"JSON Text": "JSON Text",
|
||||||
"JSON-based access control rules. Leave empty to allow all users.": "JSON-based access control rules. Leave empty to allow all users.",
|
"JSON-based access control rules. Leave empty to allow all users.": "JSON-based access control rules. Leave empty to allow all users.",
|
||||||
"Just now": "Just now",
|
"Just now": "Just now",
|
||||||
@@ -4313,6 +4319,7 @@
|
|||||||
"Validity Period": "Validity Period",
|
"Validity Period": "Validity Period",
|
||||||
"Value": "Value",
|
"Value": "Value",
|
||||||
"Value (supports JSON or plain text)": "Value (supports JSON or plain text)",
|
"Value (supports JSON or plain text)": "Value (supports JSON or plain text)",
|
||||||
|
"Value is required": "Value is required",
|
||||||
"Value must be at least 0": "Value must be at least 0",
|
"Value must be at least 0": "Value must be at least 0",
|
||||||
"Value Regex": "Value Regex",
|
"Value Regex": "Value Regex",
|
||||||
"variable": "variable",
|
"variable": "variable",
|
||||||
|
|||||||
Vendored
+7
@@ -679,6 +679,7 @@
|
|||||||
"Check for updates": "Vérifier les mises à jour",
|
"Check for updates": "Vérifier les mises à jour",
|
||||||
"Check in daily to receive random quota rewards": "Connectez-vous quotidiennement pour recevoir des récompenses de quota aléatoires",
|
"Check in daily to receive random quota rewards": "Connectez-vous quotidiennement pour recevoir des récompenses de quota aléatoires",
|
||||||
"Check in now": "Se connecter maintenant",
|
"Check in now": "Se connecter maintenant",
|
||||||
|
"Check line {{line}} for a missing comma.": "Vérifiez la ligne {{line}} pour une virgule manquante.",
|
||||||
"Check out the Quick Start": "Consultez le démarrage rapide",
|
"Check out the Quick Start": "Consultez le démarrage rapide",
|
||||||
"Check resolved IPs against IP filters even when accessing by domain": "Vérifier les adresses IP résolues par rapport aux filtres IP même lors de l'accès par domaine",
|
"Check resolved IPs against IP filters even when accessing by domain": "Vérifier les adresses IP résolues par rapport aux filtres IP même lors de l'accès par domaine",
|
||||||
"Check-in failed": "Échec de la connexion",
|
"Check-in failed": "Échec de la connexion",
|
||||||
@@ -1527,6 +1528,7 @@
|
|||||||
"Expand": "Développer",
|
"Expand": "Développer",
|
||||||
"Expand All": "Tout développer",
|
"Expand All": "Tout développer",
|
||||||
"Expected a JSON array.": "Un tableau JSON est attendu.",
|
"Expected a JSON array.": "Un tableau JSON est attendu.",
|
||||||
|
"Expected a JSON array of group identifiers": "Un tableau JSON d'identifiants de groupe est attendu",
|
||||||
"Experiment with prompts and models in real time.": "Expérimentez avec des prompts et des modèles en temps réel.",
|
"Experiment with prompts and models in real time.": "Expérimentez avec des prompts et des modèles en temps réel.",
|
||||||
"Expiration Time": "Heure d'expiration",
|
"Expiration Time": "Heure d'expiration",
|
||||||
"expired": "expiré",
|
"expired": "expiré",
|
||||||
@@ -2093,6 +2095,9 @@
|
|||||||
"JSON Editor": "Édition JSON",
|
"JSON Editor": "Édition JSON",
|
||||||
"JSON format error": "Erreur de format JSON",
|
"JSON format error": "Erreur de format JSON",
|
||||||
"JSON format supports service account JSON files": "Le format JSON prend en charge les fichiers JSON de compte de service",
|
"JSON format supports service account JSON files": "Le format JSON prend en charge les fichiers JSON de compte de service",
|
||||||
|
"JSON is invalid at line {{line}}, column {{column}}.": "Le JSON est invalide à la ligne {{line}}, colonne {{column}}.",
|
||||||
|
"JSON is invalid at position {{position}}.": "Le JSON est invalide à la position {{position}}.",
|
||||||
|
"JSON is invalid. Please check the syntax.": "Le JSON est invalide. Veuillez vérifier la syntaxe.",
|
||||||
"JSON map of group → description exposed when users create API keys.": "Carte JSON de groupe → description exposée lorsque les utilisateurs créent des clés API.",
|
"JSON map of group → description exposed when users create API keys.": "Carte JSON de groupe → description exposée lorsque les utilisateurs créent des clés API.",
|
||||||
"JSON map of group → ratio applied when the user selects the group explicitly.": "Carte JSON de groupe → ratio appliqué lorsque l'utilisateur sélectionne explicitement le groupe.",
|
"JSON map of group → ratio applied when the user selects the group explicitly.": "Carte JSON de groupe → ratio appliqué lorsque l'utilisateur sélectionne explicitement le groupe.",
|
||||||
"JSON map of model → multiplier applied to quota billing.": "Carte JSON de modèle → multiplicateur appliqué à la facturation par quota.",
|
"JSON map of model → multiplier applied to quota billing.": "Carte JSON de modèle → multiplicateur appliqué à la facturation par quota.",
|
||||||
@@ -2101,6 +2106,7 @@
|
|||||||
"JSON Mode": "Mode JSON",
|
"JSON Mode": "Mode JSON",
|
||||||
"JSON must be an object": "Le JSON doit être un objet",
|
"JSON must be an object": "Le JSON doit être un objet",
|
||||||
"JSON object:": "Objet JSON :",
|
"JSON object:": "Objet JSON :",
|
||||||
|
"JSON structure is invalid": "La structure JSON est invalide",
|
||||||
"JSON Text": "Texte JSON",
|
"JSON Text": "Texte JSON",
|
||||||
"JSON-based access control rules. Leave empty to allow all users.": "Règles de contrôle d'accès basées sur JSON. Laisser vide pour autoriser tous les utilisateurs.",
|
"JSON-based access control rules. Leave empty to allow all users.": "Règles de contrôle d'accès basées sur JSON. Laisser vide pour autoriser tous les utilisateurs.",
|
||||||
"Just now": "À l'instant",
|
"Just now": "À l'instant",
|
||||||
@@ -4313,6 +4319,7 @@
|
|||||||
"Validity Period": "Période de validité",
|
"Validity Period": "Période de validité",
|
||||||
"Value": "Valeur",
|
"Value": "Valeur",
|
||||||
"Value (supports JSON or plain text)": "Valeur (JSON ou texte brut)",
|
"Value (supports JSON or plain text)": "Valeur (JSON ou texte brut)",
|
||||||
|
"Value is required": "La valeur est obligatoire",
|
||||||
"Value must be at least 0": "La valeur doit être au moins 0",
|
"Value must be at least 0": "La valeur doit être au moins 0",
|
||||||
"Value Regex": "Regex de valeur",
|
"Value Regex": "Regex de valeur",
|
||||||
"variable": "variable",
|
"variable": "variable",
|
||||||
|
|||||||
Vendored
+7
@@ -679,6 +679,7 @@
|
|||||||
"Check for updates": "更新を確認",
|
"Check for updates": "更新を確認",
|
||||||
"Check in daily to receive random quota rewards": "毎日チェックインして、ランダムなノルマ報酬を受け取りましょう",
|
"Check in daily to receive random quota rewards": "毎日チェックインして、ランダムなノルマ報酬を受け取りましょう",
|
||||||
"Check in now": "今すぐチェックイン",
|
"Check in now": "今すぐチェックイン",
|
||||||
|
"Check line {{line}} for a missing comma.": "{{line}} 行目にカンマの抜けがないか確認してください。",
|
||||||
"Check out the Quick Start": "クイックスタートをご確認ください",
|
"Check out the Quick Start": "クイックスタートをご確認ください",
|
||||||
"Check resolved IPs against IP filters even when accessing by domain": "ドメインによるアクセスであっても、解決されたIPをIPフィルターと照合してチェックします",
|
"Check resolved IPs against IP filters even when accessing by domain": "ドメインによるアクセスであっても、解決されたIPをIPフィルターと照合してチェックします",
|
||||||
"Check-in failed": "チェックインできませんでした",
|
"Check-in failed": "チェックインできませんでした",
|
||||||
@@ -1527,6 +1528,7 @@
|
|||||||
"Expand": "展開",
|
"Expand": "展開",
|
||||||
"Expand All": "すべて展開",
|
"Expand All": "すべて展開",
|
||||||
"Expected a JSON array.": "JSON 配列が必要です。",
|
"Expected a JSON array.": "JSON 配列が必要です。",
|
||||||
|
"Expected a JSON array of group identifiers": "グループ識別子の JSON 配列が必要です",
|
||||||
"Experiment with prompts and models in real time.": "プロンプトとモデルをリアルタイムで実験する。",
|
"Experiment with prompts and models in real time.": "プロンプトとモデルをリアルタイムで実験する。",
|
||||||
"Expiration Time": "有効期限",
|
"Expiration Time": "有効期限",
|
||||||
"expired": "期限切れ",
|
"expired": "期限切れ",
|
||||||
@@ -2093,6 +2095,9 @@
|
|||||||
"JSON Editor": "JSON編集",
|
"JSON Editor": "JSON編集",
|
||||||
"JSON format error": "JSONフォーマットエラー",
|
"JSON format error": "JSONフォーマットエラー",
|
||||||
"JSON format supports service account JSON files": "JSON形式はサービスアカウントJSONファイルをサポートします",
|
"JSON format supports service account JSON files": "JSON形式はサービスアカウントJSONファイルをサポートします",
|
||||||
|
"JSON is invalid at line {{line}}, column {{column}}.": "JSON は {{line}} 行目、{{column}} 列目で無効です。",
|
||||||
|
"JSON is invalid at position {{position}}.": "JSON は位置 {{position}} で無効です。",
|
||||||
|
"JSON is invalid. Please check the syntax.": "JSON が無効です。構文を確認してください。",
|
||||||
"JSON map of group → description exposed when users create API keys.": "ユーザーがAPIキーを作成する際に公開される、グループ → 説明のJSONマップ。",
|
"JSON map of group → description exposed when users create API keys.": "ユーザーがAPIキーを作成する際に公開される、グループ → 説明のJSONマップ。",
|
||||||
"JSON map of group → ratio applied when the user selects the group explicitly.": "ユーザーがグループを明示的に選択したときに適用される、グループ → 比率のJSONマップ。",
|
"JSON map of group → ratio applied when the user selects the group explicitly.": "ユーザーがグループを明示的に選択したときに適用される、グループ → 比率のJSONマップ。",
|
||||||
"JSON map of model → multiplier applied to quota billing.": "モデル → クォータ請求に適用される乗数のJSONマップ。",
|
"JSON map of model → multiplier applied to quota billing.": "モデル → クォータ請求に適用される乗数のJSONマップ。",
|
||||||
@@ -2101,6 +2106,7 @@
|
|||||||
"JSON Mode": "JSONモード",
|
"JSON Mode": "JSONモード",
|
||||||
"JSON must be an object": "JSON はオブジェクトである必要があります",
|
"JSON must be an object": "JSON はオブジェクトである必要があります",
|
||||||
"JSON object:": "JSONオブジェクト:",
|
"JSON object:": "JSONオブジェクト:",
|
||||||
|
"JSON structure is invalid": "JSON 構造が無効です",
|
||||||
"JSON Text": "JSONテキスト",
|
"JSON Text": "JSONテキスト",
|
||||||
"JSON-based access control rules. Leave empty to allow all users.": "JSONベースのアクセス制御ルール。すべてのユーザーを許可する場合は空のままにしてください。",
|
"JSON-based access control rules. Leave empty to allow all users.": "JSONベースのアクセス制御ルール。すべてのユーザーを許可する場合は空のままにしてください。",
|
||||||
"Just now": "たった今",
|
"Just now": "たった今",
|
||||||
@@ -4313,6 +4319,7 @@
|
|||||||
"Validity Period": "有効期間",
|
"Validity Period": "有効期間",
|
||||||
"Value": "値",
|
"Value": "値",
|
||||||
"Value (supports JSON or plain text)": "値(JSONまたはプレーンテキスト対応)",
|
"Value (supports JSON or plain text)": "値(JSONまたはプレーンテキスト対応)",
|
||||||
|
"Value is required": "値は必須です",
|
||||||
"Value must be at least 0": "値は 0 以上である必要があります",
|
"Value must be at least 0": "値は 0 以上である必要があります",
|
||||||
"Value Regex": "Value 正規表現",
|
"Value Regex": "Value 正規表現",
|
||||||
"variable": "変数",
|
"variable": "変数",
|
||||||
|
|||||||
Vendored
+7
@@ -679,6 +679,7 @@
|
|||||||
"Check for updates": "Проверить обновления",
|
"Check for updates": "Проверить обновления",
|
||||||
"Check in daily to receive random quota rewards": "Регистрируйтесь ежедневно, чтобы получать случайные вознаграждения по квоте",
|
"Check in daily to receive random quota rewards": "Регистрируйтесь ежедневно, чтобы получать случайные вознаграждения по квоте",
|
||||||
"Check in now": "Войдите сейчас",
|
"Check in now": "Войдите сейчас",
|
||||||
|
"Check line {{line}} for a missing comma.": "Проверьте строку {{line}} на пропущенную запятую.",
|
||||||
"Check out the Quick Start": "Ознакомьтесь с быстрым стартом",
|
"Check out the Quick Start": "Ознакомьтесь с быстрым стартом",
|
||||||
"Check resolved IPs against IP filters even when accessing by domain": "Проверять разрешенные IP-адреса по IP-фильтрам даже при доступе по домену",
|
"Check resolved IPs against IP filters even when accessing by domain": "Проверять разрешенные IP-адреса по IP-фильтрам даже при доступе по домену",
|
||||||
"Check-in failed": "Регистрация не удалась.",
|
"Check-in failed": "Регистрация не удалась.",
|
||||||
@@ -1527,6 +1528,7 @@
|
|||||||
"Expand": "Развернуть",
|
"Expand": "Развернуть",
|
||||||
"Expand All": "Развернуть все",
|
"Expand All": "Развернуть все",
|
||||||
"Expected a JSON array.": "Ожидается JSON-массив.",
|
"Expected a JSON array.": "Ожидается JSON-массив.",
|
||||||
|
"Expected a JSON array of group identifiers": "Ожидается JSON-массив идентификаторов групп",
|
||||||
"Experiment with prompts and models in real time.": "Экспериментируйте с промптами и моделями в реальном времени.",
|
"Experiment with prompts and models in real time.": "Экспериментируйте с промптами и моделями в реальном времени.",
|
||||||
"Expiration Time": "Время истечения срока действия",
|
"Expiration Time": "Время истечения срока действия",
|
||||||
"expired": "истек",
|
"expired": "истек",
|
||||||
@@ -2093,6 +2095,9 @@
|
|||||||
"JSON Editor": "Редактирование JSON",
|
"JSON Editor": "Редактирование JSON",
|
||||||
"JSON format error": "Ошибка формата JSON",
|
"JSON format error": "Ошибка формата JSON",
|
||||||
"JSON format supports service account JSON files": "Формат JSON поддерживает JSON-файлы сервисного аккаунта",
|
"JSON format supports service account JSON files": "Формат JSON поддерживает JSON-файлы сервисного аккаунта",
|
||||||
|
"JSON is invalid at line {{line}}, column {{column}}.": "JSON недействителен в строке {{line}}, столбце {{column}}.",
|
||||||
|
"JSON is invalid at position {{position}}.": "JSON недействителен в позиции {{position}}.",
|
||||||
|
"JSON is invalid. Please check the syntax.": "JSON недействителен. Проверьте синтаксис.",
|
||||||
"JSON map of group → description exposed when users create API keys.": "JSON-карта группы → описание, отображаемое при создании пользователями ключей API.",
|
"JSON map of group → description exposed when users create API keys.": "JSON-карта группы → описание, отображаемое при создании пользователями ключей API.",
|
||||||
"JSON map of group → ratio applied when the user selects the group explicitly.": "JSON-карта группы → соотношение, применяемое, когда пользователь явно выбирает группу.",
|
"JSON map of group → ratio applied when the user selects the group explicitly.": "JSON-карта группы → соотношение, применяемое, когда пользователь явно выбирает группу.",
|
||||||
"JSON map of model → multiplier applied to quota billing.": "JSON-карта модели → множитель, применяемый к тарификации по квоте.",
|
"JSON map of model → multiplier applied to quota billing.": "JSON-карта модели → множитель, применяемый к тарификации по квоте.",
|
||||||
@@ -2101,6 +2106,7 @@
|
|||||||
"JSON Mode": "Режим JSON",
|
"JSON Mode": "Режим JSON",
|
||||||
"JSON must be an object": "JSON должен быть объектом",
|
"JSON must be an object": "JSON должен быть объектом",
|
||||||
"JSON object:": "Объект JSON:",
|
"JSON object:": "Объект JSON:",
|
||||||
|
"JSON structure is invalid": "Структура JSON недействительна",
|
||||||
"JSON Text": "JSON текст",
|
"JSON Text": "JSON текст",
|
||||||
"JSON-based access control rules. Leave empty to allow all users.": "Правила контроля доступа на основе JSON. Оставьте пустым, чтобы разрешить всем пользователям.",
|
"JSON-based access control rules. Leave empty to allow all users.": "Правила контроля доступа на основе JSON. Оставьте пустым, чтобы разрешить всем пользователям.",
|
||||||
"Just now": "Только что",
|
"Just now": "Только что",
|
||||||
@@ -4313,6 +4319,7 @@
|
|||||||
"Validity Period": "Срок действия",
|
"Validity Period": "Срок действия",
|
||||||
"Value": "Значение",
|
"Value": "Значение",
|
||||||
"Value (supports JSON or plain text)": "Значение (JSON или текст)",
|
"Value (supports JSON or plain text)": "Значение (JSON или текст)",
|
||||||
|
"Value is required": "Значение обязательно",
|
||||||
"Value must be at least 0": "Значение должно быть не менее 0",
|
"Value must be at least 0": "Значение должно быть не менее 0",
|
||||||
"Value Regex": "Регулярное выражение значения",
|
"Value Regex": "Регулярное выражение значения",
|
||||||
"variable": "переменная",
|
"variable": "переменная",
|
||||||
|
|||||||
Vendored
+7
@@ -679,6 +679,7 @@
|
|||||||
"Check for updates": "Kiểm tra cập nhật",
|
"Check for updates": "Kiểm tra cập nhật",
|
||||||
"Check in daily to receive random quota rewards": "Nhận phòng hàng ngày để nhận phần thưởng theo hạn ngạch ngẫu nhiên",
|
"Check in daily to receive random quota rewards": "Nhận phòng hàng ngày để nhận phần thưởng theo hạn ngạch ngẫu nhiên",
|
||||||
"Check in now": "Điểm danh ngay",
|
"Check in now": "Điểm danh ngay",
|
||||||
|
"Check line {{line}} for a missing comma.": "Kiểm tra dòng {{line}} xem có thiếu dấu phẩy không.",
|
||||||
"Check out the Quick Start": "Xem hướng dẫn bắt đầu nhanh",
|
"Check out the Quick Start": "Xem hướng dẫn bắt đầu nhanh",
|
||||||
"Check resolved IPs against IP filters even when accessing by domain": "Kiểm tra các IP đã phân giải đối chiếu với các bộ lọc IP ngay cả khi truy cập bằng tên miền",
|
"Check resolved IPs against IP filters even when accessing by domain": "Kiểm tra các IP đã phân giải đối chiếu với các bộ lọc IP ngay cả khi truy cập bằng tên miền",
|
||||||
"Check-in failed": "Điểm danh thất bại",
|
"Check-in failed": "Điểm danh thất bại",
|
||||||
@@ -1527,6 +1528,7 @@
|
|||||||
"Expand": "Mở rộng",
|
"Expand": "Mở rộng",
|
||||||
"Expand All": "Mở rộng tất cả",
|
"Expand All": "Mở rộng tất cả",
|
||||||
"Expected a JSON array.": "Cần là một mảng JSON.",
|
"Expected a JSON array.": "Cần là một mảng JSON.",
|
||||||
|
"Expected a JSON array of group identifiers": "Cần là một mảng JSON gồm các định danh nhóm",
|
||||||
"Experiment with prompts and models in real time.": "Thử nghiệm với prompt và mô hình theo thời gian thực.",
|
"Experiment with prompts and models in real time.": "Thử nghiệm với prompt và mô hình theo thời gian thực.",
|
||||||
"Expiration Time": "Thời gian hết hạn",
|
"Expiration Time": "Thời gian hết hạn",
|
||||||
"expired": "Đã hết hạn",
|
"expired": "Đã hết hạn",
|
||||||
@@ -2093,6 +2095,9 @@
|
|||||||
"JSON Editor": "Trình chỉnh sửa JSON",
|
"JSON Editor": "Trình chỉnh sửa JSON",
|
||||||
"JSON format error": "Lỗi định dạng JSON",
|
"JSON format error": "Lỗi định dạng JSON",
|
||||||
"JSON format supports service account JSON files": "Định dạng JSON hỗ trợ các tệp JSON tài khoản dịch vụ",
|
"JSON format supports service account JSON files": "Định dạng JSON hỗ trợ các tệp JSON tài khoản dịch vụ",
|
||||||
|
"JSON is invalid at line {{line}}, column {{column}}.": "JSON không hợp lệ tại dòng {{line}}, cột {{column}}.",
|
||||||
|
"JSON is invalid at position {{position}}.": "JSON không hợp lệ tại vị trí {{position}}.",
|
||||||
|
"JSON is invalid. Please check the syntax.": "JSON không hợp lệ. Vui lòng kiểm tra cú pháp.",
|
||||||
"JSON map of group → description exposed when users create API keys.": "Ánh xạ JSON của nhóm → mô tả được hiển thị khi người dùng tạo khóa API.",
|
"JSON map of group → description exposed when users create API keys.": "Ánh xạ JSON của nhóm → mô tả được hiển thị khi người dùng tạo khóa API.",
|
||||||
"JSON map of group → ratio applied when the user selects the group explicitly.": "Bản đồ JSON của nhóm → tỷ lệ được áp dụng khi người dùng chọn nhóm đó một cách rõ ràng.",
|
"JSON map of group → ratio applied when the user selects the group explicitly.": "Bản đồ JSON của nhóm → tỷ lệ được áp dụng khi người dùng chọn nhóm đó một cách rõ ràng.",
|
||||||
"JSON map of model → multiplier applied to quota billing.": "Bản đồ JSON của mô hình → hệ số nhân áp dụng cho thanh toán hạn mức.",
|
"JSON map of model → multiplier applied to quota billing.": "Bản đồ JSON của mô hình → hệ số nhân áp dụng cho thanh toán hạn mức.",
|
||||||
@@ -2101,6 +2106,7 @@
|
|||||||
"JSON Mode": "Chế độ JSON",
|
"JSON Mode": "Chế độ JSON",
|
||||||
"JSON must be an object": "JSON phải là object",
|
"JSON must be an object": "JSON phải là object",
|
||||||
"JSON object:": "Đối tượng JSON:",
|
"JSON object:": "Đối tượng JSON:",
|
||||||
|
"JSON structure is invalid": "Cấu trúc JSON không hợp lệ",
|
||||||
"JSON Text": "Văn bản JSON",
|
"JSON Text": "Văn bản JSON",
|
||||||
"JSON-based access control rules. Leave empty to allow all users.": "Quy tắc kiểm soát truy cập dựa trên JSON. Để trống để cho phép tất cả người dùng.",
|
"JSON-based access control rules. Leave empty to allow all users.": "Quy tắc kiểm soát truy cập dựa trên JSON. Để trống để cho phép tất cả người dùng.",
|
||||||
"Just now": "Vừa nãy",
|
"Just now": "Vừa nãy",
|
||||||
@@ -4313,6 +4319,7 @@
|
|||||||
"Validity Period": "Thời hạn hiệu lực",
|
"Validity Period": "Thời hạn hiệu lực",
|
||||||
"Value": "Giá trị",
|
"Value": "Giá trị",
|
||||||
"Value (supports JSON or plain text)": "Giá trị (hỗ trợ JSON hoặc văn bản thuần)",
|
"Value (supports JSON or plain text)": "Giá trị (hỗ trợ JSON hoặc văn bản thuần)",
|
||||||
|
"Value is required": "Giá trị là bắt buộc",
|
||||||
"Value must be at least 0": "Giá trị phải ít nhất là 0",
|
"Value must be at least 0": "Giá trị phải ít nhất là 0",
|
||||||
"Value Regex": "Regex giá trị",
|
"Value Regex": "Regex giá trị",
|
||||||
"variable": "biến",
|
"variable": "biến",
|
||||||
|
|||||||
Vendored
+7
@@ -679,6 +679,7 @@
|
|||||||
"Check for updates": "检查更新",
|
"Check for updates": "检查更新",
|
||||||
"Check in daily to receive random quota rewards": "每日签到可获得随机额度奖励",
|
"Check in daily to receive random quota rewards": "每日签到可获得随机额度奖励",
|
||||||
"Check in now": "立即签到",
|
"Check in now": "立即签到",
|
||||||
|
"Check line {{line}} for a missing comma.": "请检查第 {{line}} 行是否缺少逗号。",
|
||||||
"Check out the Quick Start": "请查看 新手入门",
|
"Check out the Quick Start": "请查看 新手入门",
|
||||||
"Check resolved IPs against IP filters even when accessing by domain": "即使通过域名访问,也对照 IP 过滤器检查解析的 IP",
|
"Check resolved IPs against IP filters even when accessing by domain": "即使通过域名访问,也对照 IP 过滤器检查解析的 IP",
|
||||||
"Check-in failed": "签到失败",
|
"Check-in failed": "签到失败",
|
||||||
@@ -1527,6 +1528,7 @@
|
|||||||
"Expand": "展开",
|
"Expand": "展开",
|
||||||
"Expand All": "全部展开",
|
"Expand All": "全部展开",
|
||||||
"Expected a JSON array.": "应为 JSON 数组。",
|
"Expected a JSON array.": "应为 JSON 数组。",
|
||||||
|
"Expected a JSON array of group identifiers": "应为分组标识符的 JSON 数组",
|
||||||
"Experiment with prompts and models in real time.": "实时实验提示词和模型。",
|
"Experiment with prompts and models in real time.": "实时实验提示词和模型。",
|
||||||
"Expiration Time": "过期时间",
|
"Expiration Time": "过期时间",
|
||||||
"expired": "已过期",
|
"expired": "已过期",
|
||||||
@@ -2093,6 +2095,9 @@
|
|||||||
"JSON Editor": "JSON 编辑",
|
"JSON Editor": "JSON 编辑",
|
||||||
"JSON format error": "JSON 格式错误",
|
"JSON format error": "JSON 格式错误",
|
||||||
"JSON format supports service account JSON files": "JSON 格式支持服务账户 JSON 文件",
|
"JSON format supports service account JSON files": "JSON 格式支持服务账户 JSON 文件",
|
||||||
|
"JSON is invalid at line {{line}}, column {{column}}.": "JSON 在第 {{line}} 行、第 {{column}} 列无效。",
|
||||||
|
"JSON is invalid at position {{position}}.": "JSON 在位置 {{position}} 无效。",
|
||||||
|
"JSON is invalid. Please check the syntax.": "JSON 无效,请检查语法。",
|
||||||
"JSON map of group → description exposed when users create API keys.": "分组 → 描述的 JSON 映射,在用户创建 API 密钥时公开。",
|
"JSON map of group → description exposed when users create API keys.": "分组 → 描述的 JSON 映射,在用户创建 API 密钥时公开。",
|
||||||
"JSON map of group → ratio applied when the user selects the group explicitly.": "分组 → 比率的 JSON 映射,当用户明确选择该分组时应用此比率。",
|
"JSON map of group → ratio applied when the user selects the group explicitly.": "分组 → 比率的 JSON 映射,当用户明确选择该分组时应用此比率。",
|
||||||
"JSON map of model → multiplier applied to quota billing.": "模型 → 应用于配额计费的乘数的 JSON 映射。",
|
"JSON map of model → multiplier applied to quota billing.": "模型 → 应用于配额计费的乘数的 JSON 映射。",
|
||||||
@@ -2101,6 +2106,7 @@
|
|||||||
"JSON Mode": "JSON 模式",
|
"JSON Mode": "JSON 模式",
|
||||||
"JSON must be an object": "JSON 必须是对象",
|
"JSON must be an object": "JSON 必须是对象",
|
||||||
"JSON object:": "JSON 对象:",
|
"JSON object:": "JSON 对象:",
|
||||||
|
"JSON structure is invalid": "JSON 结构无效",
|
||||||
"JSON Text": "JSON 文本",
|
"JSON Text": "JSON 文本",
|
||||||
"JSON-based access control rules. Leave empty to allow all users.": "基于 JSON 的访问控制规则。留空以允许所有用户。",
|
"JSON-based access control rules. Leave empty to allow all users.": "基于 JSON 的访问控制规则。留空以允许所有用户。",
|
||||||
"Just now": "刚刚",
|
"Just now": "刚刚",
|
||||||
@@ -4313,6 +4319,7 @@
|
|||||||
"Validity Period": "有效期",
|
"Validity Period": "有效期",
|
||||||
"Value": "值",
|
"Value": "值",
|
||||||
"Value (supports JSON or plain text)": "值(支持 JSON 或普通文本)",
|
"Value (supports JSON or plain text)": "值(支持 JSON 或普通文本)",
|
||||||
|
"Value is required": "值为必填项",
|
||||||
"Value must be at least 0": "值必须至少为 0",
|
"Value must be at least 0": "值必须至少为 0",
|
||||||
"Value Regex": "Value 正则",
|
"Value Regex": "Value 正则",
|
||||||
"variable": "变量",
|
"variable": "变量",
|
||||||
|
|||||||
Vendored
+1
@@ -88,6 +88,7 @@ export const STATIC_I18N_KEYS = [
|
|||||||
'Failed to delete API key',
|
'Failed to delete API key',
|
||||||
'Failed to delete API keys',
|
'Failed to delete API keys',
|
||||||
'Failed to update API key status',
|
'Failed to update API key status',
|
||||||
|
'Expected a JSON array of group identifiers',
|
||||||
'Successfully created {{count}} API Key(s)',
|
'Successfully created {{count}} API Key(s)',
|
||||||
'Successfully deleted {{count}} API key(s)',
|
'Successfully deleted {{count}} API key(s)',
|
||||||
'Enter API key for this channel',
|
'Enter API key for this channel',
|
||||||
|
|||||||
Reference in New Issue
Block a user