Commit 2154fce0 by CaIon

feat(payment): enhance payment method configuration with icons and improve UI interactions

parent a2f3ac02
...@@ -19,18 +19,18 @@ var USDExchangeRate = 7.3 ...@@ -19,18 +19,18 @@ var USDExchangeRate = 7.3
var PayMethods = []map[string]string{ var PayMethods = []map[string]string{
{ {
"name": "支付宝", "name": "支付宝",
"color": "rgba(var(--semi-blue-5), 1)", "icon": "SiAlipay",
"type": "alipay", "type": "alipay",
}, },
{ {
"name": "微信", "name": "微信",
"color": "rgba(var(--semi-green-5), 1)", "icon": "SiWechat",
"type": "wxpay", "type": "wxpay",
}, },
{ {
"name": "自定义1", "name": "自定义1",
"color": "black", "icon": "LuCreditCard",
"type": "custom1", "type": "custom1",
"min_topup": "50", "min_topup": "50",
}, },
......
...@@ -39,6 +39,7 @@ const BRAND_AND_LITERAL_KEYS = new Set([ ...@@ -39,6 +39,7 @@ const BRAND_AND_LITERAL_KEYS = new Set([
'AccessKey / SecretAccessKey', 'AccessKey / SecretAccessKey',
'AZURE_OPENAI_ENDPOINT *', 'AZURE_OPENAI_ENDPOINT *',
'Baidu V2', 'Baidu V2',
'CC Switch',
'ChatGPT', 'ChatGPT',
'ChatGPT Subscription (Codex)', 'ChatGPT Subscription (Codex)',
'Claude', 'Claude',
...@@ -317,4 +318,3 @@ main().catch((err) => { ...@@ -317,4 +318,3 @@ main().catch((err) => {
console.error(err) console.error(err)
process.exitCode = 1 process.exitCode = 1
}) })
/*
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 { useEffect, useState } from 'react'
import type { IconBaseProps, IconType } from 'react-icons'
type IconPackModule = Record<string, unknown>
type IconPackLoader = () => Promise<IconPackModule>
const ICON_PACK_LOADERS = {
ai: () => import('react-icons/ai').then((module) => module as IconPackModule),
bi: () => import('react-icons/bi').then((module) => module as IconPackModule),
bs: () => import('react-icons/bs').then((module) => module as IconPackModule),
cg: () => import('react-icons/cg').then((module) => module as IconPackModule),
ci: () => import('react-icons/ci').then((module) => module as IconPackModule),
di: () => import('react-icons/di').then((module) => module as IconPackModule),
fa: () => import('react-icons/fa').then((module) => module as IconPackModule),
fa6: () =>
import('react-icons/fa6').then((module) => module as IconPackModule),
fc: () => import('react-icons/fc').then((module) => module as IconPackModule),
fi: () => import('react-icons/fi').then((module) => module as IconPackModule),
gi: () => import('react-icons/gi').then((module) => module as IconPackModule),
go: () => import('react-icons/go').then((module) => module as IconPackModule),
gr: () => import('react-icons/gr').then((module) => module as IconPackModule),
hi: () => import('react-icons/hi').then((module) => module as IconPackModule),
hi2: () =>
import('react-icons/hi2').then((module) => module as IconPackModule),
im: () => import('react-icons/im').then((module) => module as IconPackModule),
io: () => import('react-icons/io').then((module) => module as IconPackModule),
io5: () =>
import('react-icons/io5').then((module) => module as IconPackModule),
lia: () =>
import('react-icons/lia').then((module) => module as IconPackModule),
lu: () => import('react-icons/lu').then((module) => module as IconPackModule),
md: () => import('react-icons/md').then((module) => module as IconPackModule),
pi: () => import('react-icons/pi').then((module) => module as IconPackModule),
ri: () => import('react-icons/ri').then((module) => module as IconPackModule),
rx: () => import('react-icons/rx').then((module) => module as IconPackModule),
si: () => import('react-icons/si').then((module) => module as IconPackModule),
sl: () => import('react-icons/sl').then((module) => module as IconPackModule),
tb: () => import('react-icons/tb').then((module) => module as IconPackModule),
tfi: () =>
import('react-icons/tfi').then((module) => module as IconPackModule),
ti: () => import('react-icons/ti').then((module) => module as IconPackModule),
vsc: () =>
import('react-icons/vsc').then((module) => module as IconPackModule),
wi: () => import('react-icons/wi').then((module) => module as IconPackModule),
} satisfies Record<string, IconPackLoader>
type IconPackId = keyof typeof ICON_PACK_LOADERS
const ICON_PACK_CACHE = new Map<IconPackId, Promise<IconPackModule>>()
const ICON_PACK_CANDIDATES: Array<[RegExp, IconPackId[]]> = [
[/^Ai[A-Z0-9]/, ['ai']],
[/^Bi[A-Z0-9]/, ['bi']],
[/^Bs[A-Z0-9]/, ['bs']],
[/^Cg[A-Z0-9]/, ['cg']],
[/^Ci[A-Z0-9]/, ['ci']],
[/^Di[A-Z0-9]/, ['di']],
[/^Fa[A-Z0-9]/, ['fa6', 'fa']],
[/^Fc[A-Z0-9]/, ['fc']],
[/^Fi[A-Z0-9]/, ['fi']],
[/^Gi[A-Z0-9]/, ['gi']],
[/^Go[A-Z0-9]/, ['go']],
[/^Gr[A-Z0-9]/, ['gr']],
[/^Hi[A-Z0-9]/, ['hi2', 'hi']],
[/^Im[A-Z0-9]/, ['im']],
[/^Io[A-Z0-9]/, ['io5', 'io']],
[/^Lia[A-Z0-9]/, ['lia']],
[/^Lu[A-Z0-9]/, ['lu']],
[/^Md[A-Z0-9]/, ['md']],
[/^Pi[A-Z0-9]/, ['pi']],
[/^Ri[A-Z0-9]/, ['ri']],
[/^Rx[A-Z0-9]/, ['rx']],
[/^Si[A-Z0-9]/, ['si']],
[/^Sl[A-Z0-9]/, ['sl']],
[/^Tb[A-Z0-9]/, ['tb']],
[/^Tfi[A-Z0-9]/, ['tfi']],
[/^Ti[A-Z0-9]/, ['ti']],
[/^Vsc[A-Z0-9]/, ['vsc']],
[/^Wi[A-Z0-9]/, ['wi']],
]
function normalizeIconName(name: string | null | undefined): string | null {
const trimmed = name?.trim()
if (!trimmed || !/^[A-Z][A-Za-z0-9]*$/.test(trimmed)) return null
return trimmed
}
function getCandidatePacks(iconName: string): IconPackId[] {
return (
ICON_PACK_CANDIDATES.find(([pattern]) => pattern.test(iconName))?.[1] ?? []
)
}
function loadIconPack(packId: IconPackId): Promise<IconPackModule> {
const cached = ICON_PACK_CACHE.get(packId)
if (cached) return cached
const promise = ICON_PACK_LOADERS[packId]()
ICON_PACK_CACHE.set(packId, promise)
return promise
}
function isIconComponent(value: unknown): value is IconType {
return typeof value === 'function'
}
async function resolveReactIcon(iconName: string): Promise<IconType | null> {
for (const packId of getCandidatePacks(iconName)) {
try {
const icon = (await loadIconPack(packId))[iconName]
if (isIconComponent(icon)) return icon
} catch {
// Missing chunks or unknown packs should behave the same as unknown names.
}
}
return null
}
type ReactIconByNameProps = IconBaseProps & {
name?: string | null
}
type ResolvedIconState = {
iconName: string
Icon: IconType | null
}
export function ReactIconByName({ name, ...props }: ReactIconByNameProps) {
const iconName = normalizeIconName(name)
const [resolvedIcon, setResolvedIcon] = useState<ResolvedIconState | null>(
null
)
useEffect(() => {
let cancelled = false
if (!iconName) return
void resolveReactIcon(iconName).then((Icon) => {
if (!cancelled) setResolvedIcon({ iconName, Icon })
})
return () => {
cancelled = true
}
}, [iconName])
if (!iconName || resolvedIcon?.iconName !== iconName || !resolvedIcon.Icon) {
return null
}
const Icon = resolvedIcon.Icon
return <Icon {...props} />
}
...@@ -30,8 +30,8 @@ import { ...@@ -30,8 +30,8 @@ import {
} from '@/components/ui/alert-dialog' } from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
type RequiredTextPart = { type RequiredTextPart = {
type: 'input' | 'static' type: 'input' | 'static'
...@@ -63,6 +63,10 @@ type RiskAcknowledgementDialogProps = { ...@@ -63,6 +63,10 @@ type RiskAcknowledgementDialogProps = {
className?: string className?: string
} }
function getRequiredTextRows(text: string) {
return Math.max(1, Math.ceil(Array.from(text).length / 42))
}
export function RiskAcknowledgementDialog({ export function RiskAcknowledgementDialog({
open, open,
onOpenChange, onOpenChange,
...@@ -90,15 +94,21 @@ export function RiskAcknowledgementDialog({ ...@@ -90,15 +94,21 @@ export function RiskAcknowledgementDialog({
const normalizedRequiredTextParts = useMemo< const normalizedRequiredTextParts = useMemo<
NormalizedRequiredTextPart[] NormalizedRequiredTextPart[]
>(() => { >(() => {
let inputIndex = 0 return requiredTextParts.reduce<{
return requiredTextParts.map((part) => { parts: NormalizedRequiredTextPart[]
if (part.type === 'input') { inputIndex: number
const normalizedPart = { ...part, inputIndex } }>(
inputIndex += 1 (acc, part) => {
return normalizedPart if (part.type !== 'input') {
} return { ...acc, parts: [...acc.parts, part] }
return part }
}) return {
parts: [...acc.parts, { ...part, inputIndex: acc.inputIndex }],
inputIndex: acc.inputIndex + 1,
}
},
{ parts: [], inputIndex: 0 }
).parts
}, [requiredTextParts]) }, [requiredTextParts])
const requiredTextInputCount = useMemo( const requiredTextInputCount = useMemo(
...@@ -114,9 +124,12 @@ export function RiskAcknowledgementDialog({ ...@@ -114,9 +124,12 @@ export function RiskAcknowledgementDialog({
useEffect(() => { useEffect(() => {
if (!open) return if (!open) return
setCheckedItems(Array(checklist.length).fill(false)) const timer = window.setTimeout(() => {
setTypedText('') setCheckedItems(Array(checklist.length).fill(false))
setTypedTextParts(Array(requiredTextInputCount).fill('')) setTypedText('')
setTypedTextParts(Array(requiredTextInputCount).fill(''))
}, 0)
return () => window.clearTimeout(timer)
}, [open, checklist.length, requiredTextInputCount]) }, [open, checklist.length, requiredTextInputCount])
const allChecked = useMemo(() => { const allChecked = useMemo(() => {
...@@ -225,21 +238,21 @@ export function RiskAcknowledgementDialog({ ...@@ -225,21 +238,21 @@ export function RiskAcknowledgementDialog({
<Label className='text-sm font-medium'> <Label className='text-sm font-medium'>
{inputPrompt ?? t('Please type the following text to confirm:')} {inputPrompt ?? t('Please type the following text to confirm:')}
</Label> </Label>
<div className='bg-background border-border rounded-md border px-3 py-2 font-mono text-sm break-all'> <div className='bg-background border-border rounded-md border px-3 py-2 font-mono text-sm leading-6 break-words whitespace-pre-wrap'>
{requiredTextToDisplay} {requiredTextToDisplay}
</div> </div>
{hasSegmentedRequiredText ? ( {hasSegmentedRequiredText ? (
<div className='flex flex-wrap items-center gap-2'> <div className='flex flex-col gap-2'>
{normalizedRequiredTextParts.map((part, index) => {normalizedRequiredTextParts.map((part, index) =>
part.type === 'static' ? ( part.type === 'static' ? (
<span <span
key={`static-${index}`} key={`static-${index}`}
className='text-muted-foreground bg-background/70 border-border rounded-md border px-2 py-1.5 font-mono text-sm select-none' className='text-muted-foreground bg-background/70 border-border w-fit rounded-md border px-2 py-1.5 font-mono text-sm select-none'
> >
{part.text} {part.text}
</span> </span>
) : ( ) : (
<Input <Textarea
key={`input-${index}`} key={`input-${index}`}
value={typedTextParts[part.inputIndex ?? 0] ?? ''} value={typedTextParts[part.inputIndex ?? 0] ?? ''}
onChange={(event) => onChange={(event) =>
...@@ -254,30 +267,27 @@ export function RiskAcknowledgementDialog({ ...@@ -254,30 +267,27 @@ export function RiskAcknowledgementDialog({
inputPlaceholder ?? inputPlaceholder ??
t('Type the confirmation text here') t('Type the confirmation text here')
} }
rows={getRequiredTextRows(part.text)}
autoFocus={open && part.inputIndex === 0} autoFocus={open && part.inputIndex === 0}
onCopy={(event) => event.preventDefault()} wrap='soft'
onCut={(event) => event.preventDefault()}
onPaste={(event) => event.preventDefault()}
onDrop={(event) => event.preventDefault()}
aria-invalid={hasTypedRequiredText && !typedMatched} aria-invalid={hasTypedRequiredText && !typedMatched}
className='w-full font-mono sm:w-64' className='min-h-10 resize-none overflow-hidden font-mono text-sm leading-6'
/> />
) )
)} )}
</div> </div>
) : ( ) : (
<Input <Textarea
value={typedText} value={typedText}
onChange={(event) => setTypedText(event.target.value)} onChange={(event) => setTypedText(event.target.value)}
placeholder={ placeholder={
inputPlaceholder ?? t('Type the confirmation text here') inputPlaceholder ?? t('Type the confirmation text here')
} }
rows={getRequiredTextRows(requiredText)}
autoFocus={open} autoFocus={open}
onCopy={(event) => event.preventDefault()} wrap='soft'
onCut={(event) => event.preventDefault()}
onPaste={(event) => event.preventDefault()}
onDrop={(event) => event.preventDefault()}
aria-invalid={hasTypedRequiredText && !typedMatched} aria-invalid={hasTypedRequiredText && !typedMatched}
className='min-h-16 resize-none overflow-hidden font-mono text-sm leading-6'
/> />
)} )}
{hasTypedRequiredText && !typedMatched ? ( {hasTypedRequiredText && !typedMatched ? (
......
...@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -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 { useEffect, useMemo } from 'react' import { useEffect } 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,12 +34,13 @@ import { ...@@ -34,12 +34,13 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Dialog } from '@/components/dialog' import { Dialog } from '@/components/dialog'
import { ReactIconByName } from '@/components/react-icon-by-name'
const createPaymentMethodDialogSchema = (t: (key: string) => string) => const createPaymentMethodDialogSchema = (t: (key: string) => string) =>
z.object({ z.object({
name: z.string().min(1, t('Payment method name is required')), name: z.string().min(1, t('Payment method name is required')),
type: z.string().min(1, t('Payment method type is required')), type: z.string().min(1, t('Payment type key is required')),
color: z.string().min(1, t('Color is required')), icon: z.string().optional(),
min_topup: z.string().optional(), min_topup: z.string().optional(),
}) })
...@@ -52,8 +53,9 @@ const PAYMENT_METHOD_FORM_ID = 'payment-method-form' ...@@ -52,8 +53,9 @@ const PAYMENT_METHOD_FORM_ID = 'payment-method-form'
export type PaymentMethodData = { export type PaymentMethodData = {
name: string name: string
type: string type: string
color: string icon?: string
min_topup?: string min_topup?: string
color?: string
} }
type PaymentMethodDialogProps = { type PaymentMethodDialogProps = {
...@@ -63,42 +65,14 @@ type PaymentMethodDialogProps = { ...@@ -63,42 +65,14 @@ type PaymentMethodDialogProps = {
editData?: PaymentMethodData | null editData?: PaymentMethodData | null
} }
const PAYMENT_TYPES = [ const PAYMENT_TYPE_ICON_NAMES: Record<string, string> = {
{ value: 'alipay', label: 'Alipay' }, alipay: 'SiAlipay',
{ value: 'wxpay', label: 'WeChat Pay' }, stripe: 'SiStripe',
{ value: 'stripe', label: 'Stripe' }, waffo_pancake: 'LuCreditCard',
] wxpay: 'SiWechat',
const getColorPreview = (color: string) => {
if (color.includes('var(--')) {
return null
}
return color
} }
const COLOR_PRESETS = [ const getDefaultIconName = (type: string) => PAYMENT_TYPE_ICON_NAMES[type] ?? ''
{ value: '#1677FF', label: 'Blue (Alipay)' },
{ value: '#07C160', label: 'Green (WeChat)' },
{ value: '#635BFF', label: 'Purple (Stripe)' },
{ value: '#1890FF', label: 'Sky Blue' },
{ value: '#52C41A', label: 'Lime Green' },
{ value: 'black', label: 'Black' },
{ value: '#FF4D4F', label: 'Red' },
{ value: '#FFA940', label: 'Orange' },
].map((preset) => {
const previewColor = getColorPreview(preset.value)
return {
...preset,
icon: previewColor ? (
<div
className='size-4 rounded border'
style={{ backgroundColor: previewColor }}
/>
) : (
<div className='bg-muted size-4 rounded border' />
),
}
})
export function PaymentMethodDialog({ export function PaymentMethodDialog({
open, open,
...@@ -109,41 +83,60 @@ export function PaymentMethodDialog({ ...@@ -109,41 +83,60 @@ export function PaymentMethodDialog({
const { t } = useTranslation() const { t } = useTranslation()
const isEditMode = !!editData const isEditMode = !!editData
const paymentMethodDialogSchema = createPaymentMethodDialogSchema(t) const paymentMethodDialogSchema = createPaymentMethodDialogSchema(t)
const paymentTypeOptions = [
{
iconName: 'SiAlipay',
label: `${t('Alipay')} (Epay: alipay)`,
name: t('Alipay'),
value: 'alipay',
},
{
iconName: 'SiWechat',
label: `${t('WeChat Pay')} (Epay: wxpay)`,
name: t('WeChat Pay'),
value: 'wxpay',
},
{
iconName: 'SiStripe',
label: `${t('Stripe')} (stripe)`,
name: t('Stripe'),
value: 'stripe',
},
{
iconName: 'LuCreditCard',
label: 'Waffo Pancake (waffo_pancake)',
name: 'Waffo Pancake',
value: 'waffo_pancake',
},
]
const getPaymentTypeOption = (value: string) =>
paymentTypeOptions.find((option) => option.value === value)
const form = useForm<PaymentMethodDialogFormValues>({ const form = useForm<PaymentMethodDialogFormValues>({
resolver: zodResolver(paymentMethodDialogSchema), resolver: zodResolver(paymentMethodDialogSchema),
defaultValues: { defaultValues: {
name: '', name: '',
type: '', type: '',
color: '', icon: '',
min_topup: '', min_topup: '',
}, },
}) })
const colorValue = form.watch('color') const iconValue = form.watch('icon')
const colorPreview = useMemo(() => {
if (!colorValue) return null
try {
// For CSS variables like rgba(var(--semi-blue-5), 1), we can't preview accurately
// but we can detect common patterns
if (colorValue.includes('var(--')) {
return null // Can't preview CSS variables reliably
}
return colorValue
} catch {
return null
}
}, [colorValue])
useEffect(() => { useEffect(() => {
if (editData) { if (editData) {
form.reset(editData) form.reset({
name: editData.name,
type: editData.type,
icon: editData.icon ?? getDefaultIconName(editData.type),
min_topup: editData.min_topup ?? '',
})
} else { } else {
form.reset({ form.reset({
name: '', name: '',
type: '', type: '',
color: '', icon: '',
min_topup: '', min_topup: '',
}) })
} }
...@@ -153,7 +146,9 @@ export function PaymentMethodDialog({ ...@@ -153,7 +146,9 @@ export function PaymentMethodDialog({
const data: PaymentMethodData = { const data: PaymentMethodData = {
name: values.name, name: values.name,
type: values.type, type: values.type,
color: values.color, }
if (values.icon && values.icon.trim() !== '') {
data.icon = values.icon.trim()
} }
if (values.min_topup && values.min_topup.trim() !== '') { if (values.min_topup && values.min_topup.trim() !== '') {
data.min_topup = values.min_topup data.min_topup = values.min_topup
...@@ -215,19 +210,46 @@ export function PaymentMethodDialog({ ...@@ -215,19 +210,46 @@ export function PaymentMethodDialog({
name='type' name='type'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('Type')}</FormLabel> <FormLabel>{t('Payment type key')}</FormLabel>
<FormControl> <FormControl>
<Combobox <Combobox
options={PAYMENT_TYPES} options={paymentTypeOptions}
value={field.value} value={field.value}
onValueChange={field.onChange} onValueChange={(value) => {
placeholder={t('Select or enter payment type')} if (value === null) return
searchPlaceholder={t('Search payment types...')} const currentIcon = form.getValues('icon')?.trim()
const currentName = form.getValues('name')?.trim()
const previousOption = getPaymentTypeOption(field.value)
const nextOption = getPaymentTypeOption(value)
field.onChange(value)
if (
nextOption?.iconName &&
(!currentIcon ||
currentIcon === previousOption?.iconName)
) {
form.setValue('icon', nextOption.iconName, {
shouldDirty: true,
})
}
if (
nextOption?.name &&
(!currentName || currentName === previousOption?.name)
) {
form.setValue('name', nextOption.name, {
shouldDirty: true,
})
}
}}
placeholder={t('Select or enter payment type key')}
searchPlaceholder={t('Search payment type keys...')}
allowCustomValue allowCustomValue
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription className='leading-relaxed'>
{t('Select from presets or type custom identifier.')} {t(
'Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.'
)}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
...@@ -236,32 +258,30 @@ export function PaymentMethodDialog({ ...@@ -236,32 +258,30 @@ export function PaymentMethodDialog({
<FormField <FormField
control={form.control} control={form.control}
name='color' name='icon'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('Color')}</FormLabel> <FormLabel>{t('Icon')}</FormLabel>
<FormControl> <FormControl>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<Combobox <Input
options={COLOR_PRESETS} placeholder={t('e.g., SiAlipay')}
value={field.value} {...field}
onValueChange={field.onChange}
placeholder={t('Select or enter color value')}
searchPlaceholder={t('Search colors...')}
allowCustomValue
className='flex-1' className='flex-1'
/> />
{colorPreview && ( {iconValue && (
<div <ReactIconByName
className='size-9 shrink-0 rounded border' name={iconValue}
style={{ backgroundColor: colorPreview }} className='text-muted-foreground size-5 shrink-0'
title={colorPreview} title={iconValue}
/> />
)} )}
</div> </div>
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{t('Select preset or enter custom CSS color value.')} {t(
'Enter a react-icons component name. Invalid names show no icon.'
)}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
......
...@@ -27,6 +27,7 @@ import { ...@@ -27,6 +27,7 @@ import {
PopoverTrigger, PopoverTrigger,
} from '@/components/ui/popover' } from '@/components/ui/popover'
import { StaticDataTable } from '@/components/data-table' import { StaticDataTable } from '@/components/data-table'
import { ReactIconByName } from '@/components/react-icon-by-name'
import { safeJsonParseWithValidation } from '../utils/json-parser' import { safeJsonParseWithValidation } from '../utils/json-parser'
import { isArray } from '../utils/json-validators' import { isArray } from '../utils/json-validators'
import { import {
...@@ -39,47 +40,70 @@ type PaymentMethodsVisualEditorProps = { ...@@ -39,47 +40,70 @@ type PaymentMethodsVisualEditorProps = {
onChange: (value: string) => void onChange: (value: string) => void
} }
const PAYMENT_TEMPLATES = [ const PAYMENT_TYPE_ICON_NAMES: Record<string, string> = {
{ alipay: 'SiAlipay',
name: 'Alipay', stripe: 'SiStripe',
template: { waffo_pancake: 'LuCreditCard',
color: 'rgba(var(--semi-blue-5), 1)', wxpay: 'SiWechat',
name: '支付宝', }
type: 'alipay',
}, function getDefaultIconName(type: string) {
}, return PAYMENT_TYPE_ICON_NAMES[type] ?? ''
{ }
name: 'WeChat Pay',
template: { function getEffectiveIconName(method: PaymentMethodData) {
color: 'rgba(var(--semi-green-5), 1)', return method.icon || getDefaultIconName(method.type)
name: '微信', }
type: 'wxpay',
},
},
{
name: 'Stripe',
template: {
color: 'rgba(var(--semi-green-5), 1)',
name: 'Stripe',
type: 'stripe',
},
},
{
name: 'Custom',
template: {
color: 'black',
min_topup: '50',
name: '自定义1',
type: 'custom1',
},
},
]
export function PaymentMethodsVisualEditor({ export function PaymentMethodsVisualEditor({
value, value,
onChange, onChange,
}: PaymentMethodsVisualEditorProps) { }: PaymentMethodsVisualEditorProps) {
const { t } = useTranslation() const { t } = useTranslation()
const paymentTemplates = [
{
name: t('Epay Alipay'),
template: {
icon: getDefaultIconName('alipay'),
name: '支付宝',
type: 'alipay',
},
},
{
name: t('Epay WeChat Pay'),
template: {
icon: getDefaultIconName('wxpay'),
name: '微信',
type: 'wxpay',
},
},
{
name: t('Stripe'),
template: {
icon: getDefaultIconName('stripe'),
min_topup: '10',
name: 'Stripe',
type: 'stripe',
},
},
{
name: 'Waffo Pancake',
template: {
icon: getDefaultIconName('waffo_pancake'),
name: 'Waffo Pancake',
type: 'waffo_pancake',
},
},
{
name: t('Custom Epay method'),
template: {
icon: 'LuCreditCard',
min_topup: '50',
name: '自定义1',
type: 'custom1',
},
},
]
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
const [dialogOpen, setDialogOpen] = useState(false) const [dialogOpen, setDialogOpen] = useState(false)
const [editData, setEditData] = useState<PaymentMethodData | null>(null) const [editData, setEditData] = useState<PaymentMethodData | null>(null)
...@@ -98,10 +122,11 @@ export function PaymentMethodsVisualEditor({ ...@@ -98,10 +122,11 @@ export function PaymentMethodsVisualEditor({
item !== null && item !== null &&
'name' in item && 'name' in item &&
'type' in item && 'type' in item &&
'color' in item &&
typeof item.name === 'string' && typeof item.name === 'string' &&
typeof item.type === 'string' && typeof item.type === 'string' &&
typeof item.color === 'string' (!('icon' in item) || typeof item.icon === 'string') &&
(!('min_topup' in item) || typeof item.min_topup === 'string') &&
(!('color' in item) || typeof item.color === 'string')
) )
}, [value]) }, [value])
...@@ -111,7 +136,8 @@ export function PaymentMethodsVisualEditor({ ...@@ -111,7 +136,8 @@ export function PaymentMethodsVisualEditor({
return paymentMethods.filter( return paymentMethods.filter(
(method) => (method) =>
method.name.toLowerCase().includes(lowerSearch) || method.name.toLowerCase().includes(lowerSearch) ||
method.type.toLowerCase().includes(lowerSearch) method.type.toLowerCase().includes(lowerSearch) ||
getEffectiveIconName(method).toLowerCase().includes(lowerSearch)
) )
}, [paymentMethods, searchText]) }, [paymentMethods, searchText])
...@@ -202,14 +228,6 @@ export function PaymentMethodsVisualEditor({ ...@@ -202,14 +228,6 @@ export function PaymentMethodsVisualEditor({
} }
} }
const getColorPreview = (color: string) => {
// For CSS variables, show a placeholder
if (color.includes('var(--')) {
return null
}
return color
}
return ( return (
<div className='space-y-4'> <div className='space-y-4'>
<div className='flex flex-col gap-3 sm:flex-row sm:items-center'> <div className='flex flex-col gap-3 sm:flex-row sm:items-center'>
...@@ -235,10 +253,10 @@ export function PaymentMethodsVisualEditor({ ...@@ -235,10 +253,10 @@ export function PaymentMethodsVisualEditor({
<PopoverContent className='w-60'> <PopoverContent className='w-60'>
<div className='space-y-2'> <div className='space-y-2'>
<p className='text-muted-foreground text-xs'> <p className='text-muted-foreground text-xs'>
{t('Quick insert common payment methods')} {t('Quick insert payment entries')}
</p> </p>
<div className='space-y-1'> <div className='space-y-1'>
{PAYMENT_TEMPLATES.map((item) => ( {paymentTemplates.map((item) => (
<Button <Button
key={item.name} key={item.name}
type='button' type='button'
...@@ -297,7 +315,7 @@ export function PaymentMethodsVisualEditor({ ...@@ -297,7 +315,7 @@ export function PaymentMethodsVisualEditor({
}, },
{ {
id: 'type', id: 'type',
header: t('Type'), header: t('Payment type key'),
cell: (method) => ( cell: (method) => (
<code className='bg-muted rounded px-1.5 py-0.5 text-sm'> <code className='bg-muted rounded px-1.5 py-0.5 text-sm'>
{method.type} {method.type}
...@@ -305,23 +323,24 @@ export function PaymentMethodsVisualEditor({ ...@@ -305,23 +323,24 @@ export function PaymentMethodsVisualEditor({
), ),
}, },
{ {
id: 'color', id: 'icon',
header: t('Color'), header: t('Icon'),
cell: (method) => { cell: (method) => {
const colorPreview = getColorPreview(method.color) const iconName = getEffectiveIconName(method)
return ( return iconName ? (
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
{colorPreview && ( <ReactIconByName
<div name={iconName}
className='size-5 shrink-0 rounded border' className='text-muted-foreground size-5 shrink-0'
style={{ backgroundColor: colorPreview }} title={iconName}
/> />
)}
<span className='text-muted-foreground truncate font-mono text-sm'> <span className='text-muted-foreground truncate font-mono text-sm'>
{method.color} {iconName}
</span> </span>
</div> </div>
) : (
<span className='text-muted-foreground text-sm'></span>
) )
}, },
}, },
...@@ -377,7 +396,8 @@ export function PaymentMethodsVisualEditor({ ...@@ -377,7 +396,8 @@ export function PaymentMethodsVisualEditor({
{/* Mobile card view */} {/* Mobile card view */}
<div className='divide-y md:hidden'> <div className='divide-y md:hidden'>
{filteredMethods.map((method, index) => { {filteredMethods.map((method, index) => {
const colorPreview = getColorPreview(method.color) const iconName = getEffectiveIconName(method)
return ( return (
<div key={`${method.type}-${index}`} className='p-4'> <div key={`${method.type}-${index}`} className='p-4'>
<div className='mb-3 flex items-start justify-between'> <div className='mb-3 flex items-start justify-between'>
...@@ -417,19 +437,22 @@ export function PaymentMethodsVisualEditor({ ...@@ -417,19 +437,22 @@ export function PaymentMethodsVisualEditor({
<div className='space-y-2 text-sm'> <div className='space-y-2 text-sm'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<span className='text-muted-foreground min-w-20'> <span className='text-muted-foreground min-w-20'>
{t('Color:')} {t('Icon')}
</span> </span>
<div className='flex items-center gap-2'> {iconName ? (
{colorPreview && ( <div className='flex min-w-0 items-center gap-2'>
<div <ReactIconByName
className='size-5 shrink-0 rounded border' name={iconName}
style={{ backgroundColor: colorPreview }} className='text-muted-foreground size-5 shrink-0'
title={iconName}
/> />
)} <span className='text-muted-foreground truncate font-mono text-xs'>
<span className='text-muted-foreground truncate font-mono text-xs'> {iconName}
{method.color} </span>
</span> </div>
</div> ) : (
<span className='text-muted-foreground text-xs'></span>
)}
</div> </div>
{method.min_topup && ( {method.min_topup && (
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
......
...@@ -42,8 +42,8 @@ import { ...@@ -42,8 +42,8 @@ import {
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { RiskAcknowledgementDialog } from '@/components/risk-acknowledgement-dialog' import { RiskAcknowledgementDialog } from '@/components/risk-acknowledgement-dialog'
import { confirmPaymentCompliance } from '../api' import { confirmPaymentCompliance } from '../api'
...@@ -78,6 +78,20 @@ import { ...@@ -78,6 +78,20 @@ import {
type WaffoSettingsValues, type WaffoSettingsValues,
} from './waffo-settings-section' } from './waffo-settings-section'
function isHttpOriginUrl(value: string) {
const trimmed = value.trim()
if (!trimmed) return true
try {
const url = new URL(trimmed)
const isHttpProtocol = url.protocol === 'http:' || url.protocol === 'https:'
const hasNoPath = url.pathname === '' || url.pathname === '/'
return isHttpProtocol && hasNoPath && !url.search && !url.hash
} catch {
return false
}
}
const paymentSchema = z.object({ const paymentSchema = z.object({
PayAddress: z.string().refine((value) => { PayAddress: z.string().refine((value) => {
const trimmed = value.trim() const trimmed = value.trim()
...@@ -88,11 +102,12 @@ const paymentSchema = z.object({ ...@@ -88,11 +102,12 @@ const paymentSchema = z.object({
EpayKey: z.string(), EpayKey: z.string(),
Price: z.coerce.number().min(0), Price: z.coerce.number().min(0),
MinTopUp: z.coerce.number().min(0), MinTopUp: z.coerce.number().min(0),
CustomCallbackAddress: z.string().refine((value) => { CustomCallbackAddress: z
const trimmed = value.trim() .string()
if (!trimmed) return true .refine(
return /^https?:\/\//.test(trimmed) isHttpOriginUrl,
}, 'Provide a valid URL starting with http:// or https://'), 'Enter only a top-level callback domain, for example https://api.example.com, without any path.'
),
PayMethods: z.string().superRefine((value, ctx) => { PayMethods: z.string().superRefine((value, ctx) => {
const error = getJsonError(value) const error = getJsonError(value)
if (error) { if (error) {
...@@ -169,6 +184,7 @@ type PaymentBaseFormValues = Omit< ...@@ -169,6 +184,7 @@ type PaymentBaseFormValues = Omit<
> >
const CURRENT_COMPLIANCE_TERMS_VERSION = 'v1' const CURRENT_COMPLIANCE_TERMS_VERSION = 'v1'
const paymentTabContentClassName = 'mt-6 min-w-0'
type PaymentComplianceDefaults = { type PaymentComplianceDefaults = {
confirmed: boolean confirmed: boolean
...@@ -857,680 +873,742 @@ export function PaymentSettingsSection({ ...@@ -857,680 +873,742 @@ export function PaymentSettingsSection({
isSaving={updateOption.isPending || isSubmitting} isSaving={updateOption.isPending || isSubmitting}
saveLabel='Save all settings' saveLabel='Save all settings'
/> />
<div className='space-y-4'> <Tabs defaultValue='general' className='min-w-0'>
<div> <div className='overflow-x-auto pb-1'>
<h3 className='text-lg font-medium'>{t('General Settings')}</h3> <TabsList className='grid min-w-[44rem] grid-cols-6'>
<p className='text-muted-foreground text-sm'> <TabsTrigger value='general'>{t('General')}</TabsTrigger>
{t('Shared configuration for all payment gateways')} <TabsTrigger value='epay'>Epay</TabsTrigger>
</p> <TabsTrigger value='stripe'>{t('Stripe')}</TabsTrigger>
</div> <TabsTrigger value='creem'>Creem</TabsTrigger>
<TabsTrigger value='waffo-pancake'>Waffo Pancake</TabsTrigger>
<div className='grid gap-6 md:grid-cols-2'> <TabsTrigger value='waffo'>Waffo</TabsTrigger>
<FormField </TabsList>
control={form.control}
name='Price'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Price (local currency / USD)')}</FormLabel>
<FormControl>
<Input
type='number'
step='0.01'
min={0}
{...safeNumberFieldProps(field)}
/>
</FormControl>
<FormDescription>
{t(
'How much to charge for each US dollar of balance (Epay)'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='MinTopUp'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Minimum top-up (USD)')}</FormLabel>
<FormControl>
<Input
type='number'
step='0.01'
min={0}
{...safeNumberFieldProps(field)}
/>
</FormControl>
<FormDescription>
{t('Smallest USD amount users can recharge (Epay)')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div> </div>
<FormField <TabsContent value='general' className={paymentTabContentClassName}>
control={form.control} <div className='space-y-4'>
name='PayMethods' <div>
render={({ field }) => ( <h3 className='text-lg font-medium'>
<FormItem> {t('General Settings')}
<div className='mb-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'> </h3>
<FormLabel>{t('Payment methods')}</FormLabel> <p className='text-muted-foreground text-sm'>
<Button {t('Shared configuration for all payment gateways')}
type='button' </p>
variant='outline' </div>
size='sm'
onClick={() => <div className='grid gap-6 md:grid-cols-2'>
setPayMethodsVisualMode(!payMethodsVisualMode) <FormField
} control={form.control}
className='w-full sm:w-auto' name='Price'
> render={({ field }) => (
{payMethodsVisualMode ? ( <FormItem>
<> <FormLabel>
<Code2 className='mr-2 h-3 w-3' /> {t('Price (local currency / USD)')}
{t('JSON Editor')} </FormLabel>
</> <FormControl>
) : ( <Input
<> type='number'
<Eye className='mr-2 h-3 w-3' /> step='0.01'
{t('Visual Editor')} min={0}
</> {...safeNumberFieldProps(field)}
)} />
</Button> </FormControl>
</div> <FormDescription>
<FormControl> {t(
{payMethodsVisualMode ? ( 'How much to charge for each US dollar of balance (Epay)'
<PaymentMethodsVisualEditor )}
value={field.value} </FormDescription>
onChange={field.onChange} <FormMessage />
/> </FormItem>
) : ( )}
<Textarea />
rows={4}
placeholder={t( <FormField
'[{"name":"支付宝","type":"alipay","color":"#1677FF"}]' control={form.control}
name='MinTopUp'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Minimum top-up (USD)')}</FormLabel>
<FormControl>
<Input
type='number'
step='0.01'
min={0}
{...safeNumberFieldProps(field)}
/>
</FormControl>
<FormDescription>
{t('Smallest USD amount users can recharge (Epay)')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name='PayMethods'
render={({ field }) => (
<FormItem>
<div className='mb-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
<FormLabel>{t('Payment methods')}</FormLabel>
<Button
type='button'
variant='outline'
size='sm'
onClick={() =>
setPayMethodsVisualMode(!payMethodsVisualMode)
}
className='w-full sm:w-auto'
>
{payMethodsVisualMode ? (
<>
<Code2 className='mr-2 h-3 w-3' />
{t('JSON Editor')}
</>
) : (
<>
<Eye className='mr-2 h-3 w-3' />
{t('Visual Editor')}
</>
)}
</Button>
</div>
<FormControl>
{payMethodsVisualMode ? (
<PaymentMethodsVisualEditor
value={field.value}
onChange={field.onChange}
/>
) : (
<Textarea
rows={4}
placeholder={t(
'[{"name":"支付宝","type":"alipay","icon":"SiAlipay"}]'
)}
{...field}
onChange={(event) =>
field.onChange(event.target.value)
}
/>
)} )}
{...field} </FormControl>
onChange={(event) => field.onChange(event.target.value)} <FormDescription>
/> {t(
'Configured as PayMethods JSON. The type value decides which payment flow is used: stripe for Stripe, waffo_pancake for Waffo Pancake, and other values are sent to Epay as the type parameter.'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className='grid gap-6 md:grid-cols-2 md:items-start'>
<FormField
control={form.control}
name='AmountOptions'
render={({ field }) => (
<FormItem>
<div className='mb-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
<FormLabel>{t('Top-up amount options')}</FormLabel>
<Button
type='button'
variant='outline'
size='sm'
onClick={() =>
setAmountOptionsVisualMode(
!amountOptionsVisualMode
)
}
className='w-full sm:w-auto'
>
{amountOptionsVisualMode ? (
<>
<Code2 className='mr-2 h-3 w-3' />
{t('JSON Editor')}
</>
) : (
<>
<Eye className='mr-2 h-3 w-3' />
{t('Visual Editor')}
</>
)}
</Button>
</div>
<FormControl>
{amountOptionsVisualMode ? (
<AmountOptionsVisualEditor
value={field.value}
onChange={field.onChange}
/>
) : (
<Textarea
rows={4}
placeholder='[10, 20, 50, 100]'
{...field}
onChange={(event) =>
field.onChange(event.target.value)
}
/>
)}
</FormControl>
<FormDescription>
{t('Preset recharge amounts (JSON array)')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='AmountDiscount'
render={({ field }) => (
<FormItem>
<div className='mb-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
<FormLabel>{t('Amount discount')}</FormLabel>
<Button
type='button'
variant='outline'
size='sm'
onClick={() =>
setAmountDiscountVisualMode(
!amountDiscountVisualMode
)
}
className='w-full sm:w-auto'
>
{amountDiscountVisualMode ? (
<>
<Code2 className='mr-2 h-3 w-3' />
{t('JSON Editor')}
</>
) : (
<>
<Eye className='mr-2 h-3 w-3' />
{t('Visual Editor')}
</>
)}
</Button>
</div>
<FormControl>
{amountDiscountVisualMode ? (
<AmountDiscountVisualEditor
value={field.value}
onChange={field.onChange}
/>
) : (
<Textarea
rows={4}
placeholder='{"100":0.95,"200":0.9}'
{...field}
onChange={(event) =>
field.onChange(event.target.value)
}
/>
)}
</FormControl>
<FormDescription>
{t('Discount map by recharge amount (JSON object)')}
</FormDescription>
<FormMessage />
</FormItem>
)} )}
</FormControl> />
<FormDescription> </div>
</div>
</TabsContent>
<TabsContent value='epay' className={paymentTabContentClassName}>
<div className='space-y-4'>
<div>
<h3 className='text-lg font-medium'>{t('Epay Gateway')}</h3>
<p className='text-muted-foreground text-sm'>
{t('Configuration for Epay payment integration')}
</p>
</div>
<Alert>
<ShieldAlert className='h-4 w-4' />
<AlertTitle>{t('Epay safety reminder')}</AlertTitle>
<AlertDescription>
{t( {t(
'Configure available payment methods. Provide a JSON array.' 'Epay is a payment protocol, not a specific official website. Verify the provider yourself and do not trust random third-party Epay deployments.'
)}
</AlertDescription>
</Alert>
<div className='grid gap-6 md:grid-cols-2'>
<FormField
control={form.control}
name='PayAddress'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Epay endpoint')}</FormLabel>
<FormControl>
<Input
placeholder={t('https://pay.example.com')}
{...field}
onChange={(event) =>
field.onChange(event.target.value)
}
/>
</FormControl>
<FormDescription>
{t('Base address provided by your Epay service')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='CustomCallbackAddress'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Callback address')}</FormLabel>
<FormControl>
<Input
placeholder={t('https://gateway.example.com')}
{...field}
onChange={(event) =>
field.onChange(event.target.value)
}
/>
</FormControl>
<FormDescription>
{t(
'Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='grid gap-6 md:grid-cols-2'>
<FormField
control={form.control}
name='EpayId'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Epay merchant ID')}</FormLabel>
<FormControl>
<Input
placeholder='10001'
autoComplete='off'
{...field}
onChange={(event) =>
field.onChange(event.target.value)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)} )}
</FormDescription> />
<FormMessage />
</FormItem> <FormField
)} control={form.control}
/> name='EpayKey'
render={({ field }) => (
<div className='grid gap-6 md:grid-cols-2 md:items-start'> <FormItem>
<FormField <FormLabel>{t('Epay secret key')}</FormLabel>
control={form.control} <FormControl>
name='AmountOptions' <Input
render={({ field }) => ( type='password'
<FormItem> placeholder={t('Enter new key to update')}
<div className='mb-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'> autoComplete='new-password'
<FormLabel>{t('Top-up amount options')}</FormLabel> {...field}
<Button onChange={(event) =>
type='button' field.onChange(event.target.value)
variant='outline' }
size='sm' />
onClick={() => </FormControl>
setAmountOptionsVisualMode(!amountOptionsVisualMode) <FormDescription>
} {t('Leave blank unless rotating the secret')}
className='w-full sm:w-auto' </FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</TabsContent>
<TabsContent value='stripe' className={paymentTabContentClassName}>
<div className='space-y-4'>
<div>
<h3 className='text-lg font-medium'>{t('Stripe Gateway')}</h3>
<p className='text-muted-foreground text-sm'>
{t('Configuration for Stripe payment integration')}
</p>
</div>
<div className='rounded-md bg-blue-50 p-4 text-sm text-blue-900 dark:bg-blue-950 dark:text-blue-100'>
<p className='mb-2 font-medium'>
{t('Webhook Configuration:')}
</p>
<ul className='list-inside list-disc space-y-1'>
<li>
{t('Webhook URL:')}{' '}
<code className='rounded bg-blue-100 px-1 py-0.5 text-xs dark:bg-blue-900'>
{'<ServerAddress>/api/stripe/webhook'}
</code>
</li>
<li>
{t('Required events:')}{' '}
<code className='rounded bg-blue-100 px-1 py-0.5 text-xs dark:bg-blue-900'>
{t('checkout.session.completed')}
</code>{' '}
{t('and')}{' '}
<code className='rounded bg-blue-100 px-1 py-0.5 text-xs dark:bg-blue-900'>
{t('checkout.session.expired')}
</code>
</li>
<li>
{t('Configure at:')}{' '}
<a
href='https://dashboard.stripe.com/developers'
target='_blank'
rel='noreferrer'
className='underline hover:no-underline'
> >
{amountOptionsVisualMode ? ( {t('Stripe Dashboard')}
<> </a>
<Code2 className='mr-2 h-3 w-3' /> </li>
{t('JSON Editor')} </ul>
</> </div>
) : (
<> <div className='grid gap-6 md:grid-cols-3'>
<Eye className='mr-2 h-3 w-3' /> <FormField
{t('Visual Editor')} control={form.control}
</> name='StripeApiSecret'
)} render={({ field }) => (
</Button> <FormItem>
</div> <FormLabel>{t('API secret')}</FormLabel>
<FormControl> <FormControl>
{amountOptionsVisualMode ? ( <Input
<AmountOptionsVisualEditor type='password'
value={field.value} placeholder={t('sk_xxx or rk_xxx')}
onChange={field.onChange} autoComplete='new-password'
{...field}
onChange={(event) =>
field.onChange(event.target.value)
}
/>
</FormControl>
<FormDescription>
{t('Stripe API key (leave blank unless updating)')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='StripeWebhookSecret'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Webhook secret')}</FormLabel>
<FormControl>
<Input
type='password'
placeholder={t('whsec_xxx')}
autoComplete='new-password'
{...field}
onChange={(event) =>
field.onChange(event.target.value)
}
/>
</FormControl>
<FormDescription>
{t(
'Webhook signing secret (leave blank unless updating)'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='StripePriceId'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Price ID')}</FormLabel>
<FormControl>
<Input
placeholder={t('price_xxx')}
{...field}
onChange={(event) =>
field.onChange(event.target.value)
}
/>
</FormControl>
<FormDescription>
{t('Stripe product price ID')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='grid gap-6 md:grid-cols-3'>
<FormField
control={form.control}
name='StripeUnitPrice'
render={({ field }) => (
<FormItem>
<FormLabel>
{t('Unit price (local currency / USD)')}
</FormLabel>
<FormControl>
<Input
type='number'
step='0.01'
min={0}
{...safeNumberFieldProps(field)}
/>
</FormControl>
<FormDescription>
{t('e.g., 8 means 8 local currency per USD')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='StripeMinTopUp'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Minimum top-up (USD)')}</FormLabel>
<FormControl>
<Input
type='number'
step='0.01'
min={0}
{...safeNumberFieldProps(field)}
/>
</FormControl>
<FormDescription>
{t('Minimum recharge amount in USD')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='StripePromotionCodesEnabled'
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{t('Promotion codes')}</FormLabel>
<FormDescription>
{t('Allow users to enter promo codes')}
</FormDescription>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</SettingsSwitchItem>
)}
/>
</div>
</div>
</TabsContent>
<TabsContent value='creem' className={paymentTabContentClassName}>
<div className='space-y-4'>
<div>
<h3 className='text-lg font-medium'>{t('Creem Gateway')}</h3>
<p className='text-muted-foreground text-sm'>
{t('Configuration for Creem payment integration')}
</p>
</div>
<div className='rounded-md bg-blue-50 p-4 text-sm text-blue-900 dark:bg-blue-950 dark:text-blue-100'>
<p className='mb-2 font-medium'>
{t('Webhook Configuration:')}
</p>
<ul className='list-inside list-disc space-y-1'>
<li>
{t('Webhook URL:')}{' '}
<code className='rounded bg-blue-100 px-1 py-0.5 text-xs dark:bg-blue-900'>
{'<ServerAddress>/api/creem/webhook'}
</code>
</li>
<li>{t('Configure in your Creem dashboard')}</li>
</ul>
</div>
<div className='grid gap-6 md:grid-cols-2'>
<FormField
control={form.control}
name='CreemApiKey'
render={({ field }) => (
<FormItem>
<FormLabel>{t('API Key')}</FormLabel>
<FormControl>
<Input
type='password'
placeholder={t('Enter Creem API key')}
autoComplete='new-password'
{...field}
onChange={(event) =>
field.onChange(event.target.value)
}
/>
</FormControl>
<FormDescription>
{t('Creem API key (leave blank unless updating)')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='CreemWebhookSecret'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Webhook Secret')}</FormLabel>
<FormControl>
<Input
type='password'
placeholder={t('Enter webhook secret')}
autoComplete='new-password'
{...field}
onChange={(event) =>
field.onChange(event.target.value)
}
/>
</FormControl>
<FormDescription>
{t(
'Webhook signing secret (leave blank unless updating)'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name='CreemTestMode'
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{t('Test Mode')}</FormLabel>
<FormDescription>
{t('Enable test mode for Creem payments')}
</FormDescription>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/> />
) : ( </FormControl>
<Textarea </SettingsSwitchItem>
rows={4} )}
placeholder='[10, 20, 50, 100]' />
{...field}
onChange={(event) => <FormField
field.onChange(event.target.value) control={form.control}
name='CreemProducts'
render={({ field }) => (
<FormItem>
<div className='mb-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
<FormLabel>{t('Products')}</FormLabel>
<Button
type='button'
variant='outline'
size='sm'
onClick={() =>
setCreemProductsVisualMode(!creemProductsVisualMode)
} }
/> className='w-full sm:w-auto'
)} >
</FormControl> {creemProductsVisualMode ? (
<FormDescription> <>
{t('Preset recharge amounts (JSON array)')} <Code2 className='mr-2 h-3 w-3' />
</FormDescription> {t('JSON Editor')}
<FormMessage /> </>
</FormItem> ) : (
)} <>
/> <Eye className='mr-2 h-3 w-3' />
{t('Visual Editor')}
<FormField </>
control={form.control} )}
name='AmountDiscount' </Button>
render={({ field }) => ( </div>
<FormItem> <FormControl>
<div className='mb-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'> {creemProductsVisualMode ? (
<FormLabel>{t('Amount discount')}</FormLabel> <CreemProductsVisualEditor
<Button value={field.value}
type='button' onChange={field.onChange}
variant='outline' />
size='sm'
onClick={() =>
setAmountDiscountVisualMode(!amountDiscountVisualMode)
}
className='w-full sm:w-auto'
>
{amountDiscountVisualMode ? (
<>
<Code2 className='mr-2 h-3 w-3' />
{t('JSON Editor')}
</>
) : ( ) : (
<> <Textarea
<Eye className='mr-2 h-3 w-3' /> rows={4}
{t('Visual Editor')} placeholder='[{"name":"Basic","productId":"prod_xxx","price":10,"quota":500000,"currency":"USD"}]'
</> {...field}
onChange={(event) =>
field.onChange(event.target.value)
}
/>
)} )}
</Button> </FormControl>
</div>
<FormControl>
{amountDiscountVisualMode ? (
<AmountDiscountVisualEditor
value={field.value}
onChange={field.onChange}
/>
) : (
<Textarea
rows={4}
placeholder='{"100":0.95,"200":0.9}'
{...field}
onChange={(event) =>
field.onChange(event.target.value)
}
/>
)}
</FormControl>
<FormDescription>
{t('Discount map by recharge amount (JSON object)')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<Separator />
<div className='space-y-4'>
<div>
<h3 className='text-lg font-medium'>{t('Epay Gateway')}</h3>
<p className='text-muted-foreground text-sm'>
{t('Configuration for Epay payment integration')}
</p>
</div>
<div className='grid gap-6 md:grid-cols-2'>
<FormField
control={form.control}
name='PayAddress'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Epay endpoint')}</FormLabel>
<FormControl>
<Input
placeholder={t('https://pay.example.com')}
{...field}
onChange={(event) => field.onChange(event.target.value)}
/>
</FormControl>
<FormDescription>
{t('Base address provided by your Epay service')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='CustomCallbackAddress'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Callback address')}</FormLabel>
<FormControl>
<Input
placeholder={t('https://gateway.example.com')}
{...field}
onChange={(event) => field.onChange(event.target.value)}
/>
</FormControl>
<FormDescription>
{t(
'Optional callback override. Leave blank to use server address'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='grid gap-6 md:grid-cols-2'>
<FormField
control={form.control}
name='EpayId'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Epay merchant ID')}</FormLabel>
<FormControl>
<Input
placeholder='10001'
autoComplete='off'
{...field}
onChange={(event) => field.onChange(event.target.value)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='EpayKey'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Epay secret key')}</FormLabel>
<FormControl>
<Input
type='password'
placeholder={t('Enter new key to update')}
autoComplete='new-password'
{...field}
onChange={(event) => field.onChange(event.target.value)}
/>
</FormControl>
<FormDescription>
{t('Leave blank unless rotating the secret')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<Separator />
<div className='space-y-4'>
<div>
<h3 className='text-lg font-medium'>{t('Stripe Gateway')}</h3>
<p className='text-muted-foreground text-sm'>
{t('Configuration for Stripe payment integration')}
</p>
</div>
<div className='rounded-md bg-blue-50 p-4 text-sm text-blue-900 dark:bg-blue-950 dark:text-blue-100'>
<p className='mb-2 font-medium'>{t('Webhook Configuration:')}</p>
<ul className='list-inside list-disc space-y-1'>
<li>
{t('Webhook URL:')}{' '}
<code className='rounded bg-blue-100 px-1 py-0.5 text-xs dark:bg-blue-900'>
{'<ServerAddress>/api/stripe/webhook'}
</code>
</li>
<li>
{t('Required events:')}{' '}
<code className='rounded bg-blue-100 px-1 py-0.5 text-xs dark:bg-blue-900'>
{t('checkout.session.completed')}
</code>{' '}
{t('and')}{' '}
<code className='rounded bg-blue-100 px-1 py-0.5 text-xs dark:bg-blue-900'>
{t('checkout.session.expired')}
</code>
</li>
<li>
{t('Configure at:')}{' '}
<a
href='https://dashboard.stripe.com/developers'
target='_blank'
rel='noreferrer'
className='underline hover:no-underline'
>
{t('Stripe Dashboard')}
</a>
</li>
</ul>
</div>
<div className='grid gap-6 md:grid-cols-3'>
<FormField
control={form.control}
name='StripeApiSecret'
render={({ field }) => (
<FormItem>
<FormLabel>{t('API secret')}</FormLabel>
<FormControl>
<Input
type='password'
placeholder={t('sk_xxx or rk_xxx')}
autoComplete='new-password'
{...field}
onChange={(event) => field.onChange(event.target.value)}
/>
</FormControl>
<FormDescription>
{t('Stripe API key (leave blank unless updating)')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='StripeWebhookSecret'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Webhook secret')}</FormLabel>
<FormControl>
<Input
type='password'
placeholder={t('whsec_xxx')}
autoComplete='new-password'
{...field}
onChange={(event) => field.onChange(event.target.value)}
/>
</FormControl>
<FormDescription>
{t(
'Webhook signing secret (leave blank unless updating)'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='StripePriceId'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Price ID')}</FormLabel>
<FormControl>
<Input
placeholder={t('price_xxx')}
{...field}
onChange={(event) => field.onChange(event.target.value)}
/>
</FormControl>
<FormDescription>
{t('Stripe product price ID')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='grid gap-6 md:grid-cols-3'>
<FormField
control={form.control}
name='StripeUnitPrice'
render={({ field }) => (
<FormItem>
<FormLabel>
{t('Unit price (local currency / USD)')}
</FormLabel>
<FormControl>
<Input
type='number'
step='0.01'
min={0}
{...safeNumberFieldProps(field)}
/>
</FormControl>
<FormDescription>
{t('e.g., 8 means 8 local currency per USD')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='StripeMinTopUp'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Minimum top-up (USD)')}</FormLabel>
<FormControl>
<Input
type='number'
step='0.01'
min={0}
{...safeNumberFieldProps(field)}
/>
</FormControl>
<FormDescription>
{t('Minimum recharge amount in USD')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='StripePromotionCodesEnabled'
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{t('Promotion codes')}</FormLabel>
<FormDescription> <FormDescription>
{t('Allow users to enter promo codes')} {t('Configure Creem products. Provide a JSON array.')}
</FormDescription> </FormDescription>
</SettingsSwitchContent> <FormMessage />
<FormControl> </FormItem>
<Switch )}
checked={field.value} />
onCheckedChange={field.onChange} </div>
/> </TabsContent>
</FormControl>
</SettingsSwitchItem> <TabsContent
)} value='waffo-pancake'
/> className={paymentTabContentClassName}
</div> >
</div> <WaffoPancakeSettingsSection
defaultValues={waffoPancakeDefaultValues}
<Separator /> values={waffoPancakeValues}
onValueChange={setWaffoPancakeValue}
<div className='space-y-4'> selectedBinding={waffoPancakeSelection}
<div> savedBinding={waffoPancakeSavedBinding}
<h3 className='text-lg font-medium'>{t('Creem Gateway')}</h3> onSelectedBindingChange={setWaffoPancakeSelection}
<p className='text-muted-foreground text-sm'>
{t('Configuration for Creem payment integration')}
</p>
</div>
<div className='rounded-md bg-blue-50 p-4 text-sm text-blue-900 dark:bg-blue-950 dark:text-blue-100'>
<p className='mb-2 font-medium'>{t('Webhook Configuration:')}</p>
<ul className='list-inside list-disc space-y-1'>
<li>
{t('Webhook URL:')}{' '}
<code className='rounded bg-blue-100 px-1 py-0.5 text-xs dark:bg-blue-900'>
{'<ServerAddress>/api/creem/webhook'}
</code>
</li>
<li>{t('Configure in your Creem dashboard')}</li>
</ul>
</div>
<div className='grid gap-6 md:grid-cols-2'>
<FormField
control={form.control}
name='CreemApiKey'
render={({ field }) => (
<FormItem>
<FormLabel>{t('API Key')}</FormLabel>
<FormControl>
<Input
type='password'
placeholder={t('Enter Creem API key')}
autoComplete='new-password'
{...field}
onChange={(event) => field.onChange(event.target.value)}
/>
</FormControl>
<FormDescription>
{t('Creem API key (leave blank unless updating)')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/> />
</TabsContent>
<FormField
control={form.control} <TabsContent value='waffo' className={paymentTabContentClassName}>
name='CreemWebhookSecret' <WaffoSettingsSection
render={({ field }) => ( values={waffoValues}
<FormItem> onValueChange={setWaffoValue}
<FormLabel>{t('Webhook Secret')}</FormLabel> payMethods={waffoPayMethods}
<FormControl> onPayMethodsChange={setWaffoPayMethods}
<Input
type='password'
placeholder={t('Enter webhook secret')}
autoComplete='new-password'
{...field}
onChange={(event) => field.onChange(event.target.value)}
/>
</FormControl>
<FormDescription>
{t(
'Webhook signing secret (leave blank unless updating)'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/> />
</div> </TabsContent>
</Tabs>
<FormField
control={form.control}
name='CreemTestMode'
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{t('Test Mode')}</FormLabel>
<FormDescription>
{t('Enable test mode for Creem payments')}
</FormDescription>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</SettingsSwitchItem>
)}
/>
<FormField
control={form.control}
name='CreemProducts'
render={({ field }) => (
<FormItem>
<div className='mb-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
<FormLabel>{t('Products')}</FormLabel>
<Button
type='button'
variant='outline'
size='sm'
onClick={() =>
setCreemProductsVisualMode(!creemProductsVisualMode)
}
className='w-full sm:w-auto'
>
{creemProductsVisualMode ? (
<>
<Code2 className='mr-2 h-3 w-3' />
{t('JSON Editor')}
</>
) : (
<>
<Eye className='mr-2 h-3 w-3' />
{t('Visual Editor')}
</>
)}
</Button>
</div>
<FormControl>
{creemProductsVisualMode ? (
<CreemProductsVisualEditor
value={field.value}
onChange={field.onChange}
/>
) : (
<Textarea
rows={4}
placeholder='[{"name":"Basic","productId":"prod_xxx","price":10,"quota":500000,"currency":"USD"}]'
{...field}
onChange={(event) => field.onChange(event.target.value)}
/>
)}
</FormControl>
<FormDescription>
{t('Configure Creem products. Provide a JSON array.')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<Separator />
<WaffoPancakeSettingsSection
defaultValues={waffoPancakeDefaultValues}
values={waffoPancakeValues}
onValueChange={setWaffoPancakeValue}
selectedBinding={waffoPancakeSelection}
savedBinding={waffoPancakeSavedBinding}
onSelectedBindingChange={setWaffoPancakeSelection}
/>
<Separator />
<WaffoSettingsSection
values={waffoValues}
onValueChange={setWaffoValue}
payMethods={waffoPayMethods}
onPayMethodsChange={setWaffoPayMethods}
/>
</SettingsForm> </SettingsForm>
</Form> </Form>
</SettingsSection> </SettingsSection>
......
...@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -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 { useState, useEffect, useCallback, useMemo } from 'react' import { useState, useCallback, useMemo } from 'react'
import { useQueryClient, useIsFetching } from '@tanstack/react-query' import { useQueryClient, useIsFetching } from '@tanstack/react-query'
import { useNavigate, getRouteApi } from '@tanstack/react-router' import { useNavigate, getRouteApi } from '@tanstack/react-router'
import { type Table } from '@tanstack/react-table' import { type Table } from '@tanstack/react-table'
...@@ -51,12 +51,57 @@ import { ...@@ -51,12 +51,57 @@ import {
import { useUsageLogsContext } from './usage-logs-provider' import { useUsageLogsContext } from './usage-logs-provider'
const route = getRouteApi('/_authenticated/usage-logs/$section') const route = getRouteApi('/_authenticated/usage-logs/$section')
const logTypeValues = ['0', '1', '2', '3', '4', '5', '6'] as const
type LogTypeValue = (typeof logTypeValues)[number] type LogTypeValue = (typeof LOG_TYPE_FILTERS)[number]['value']
const logTypeValueSet = new Set<string>(
LOG_TYPE_FILTERS.map((type) => type.value)
)
type CommonLogDraft = {
sourceKey: string
filters: CommonLogFilters
logType: LogTypeValue
}
function isLogTypeValue(value: string): value is LogTypeValue { function isLogTypeValue(value: string): value is LogTypeValue {
return (logTypeValues as readonly string[]).includes(value) return logTypeValueSet.has(value)
}
function getLogTypeValue(value: unknown): LogTypeValue {
return Array.isArray(value) &&
value.length === 1 &&
typeof value[0] === 'string' &&
isLogTypeValue(value[0])
? value[0]
: LOG_TYPE_ALL_VALUE
}
function buildSearchSourceKey(values: {
startTime?: unknown
endTime?: unknown
channel?: unknown
model?: unknown
token?: unknown
group?: unknown
username?: unknown
requestId?: unknown
upstreamRequestId?: unknown
type?: unknown
}) {
return [
values.startTime,
values.endTime,
values.channel,
values.model,
values.token,
values.group,
values.username,
values.requestId,
values.upstreamRequestId,
Array.isArray(values.type) ? values.type.join(',') : values.type,
]
.map((value) => String(value ?? ''))
.join('\u001f')
} }
interface CommonLogsFilterBarProps<TData> { interface CommonLogsFilterBarProps<TData> {
...@@ -74,15 +119,21 @@ export function CommonLogsFilterBar<TData>( ...@@ -74,15 +119,21 @@ export function CommonLogsFilterBar<TData>(
const { sensitiveVisible, setSensitiveVisible } = useUsageLogsContext() const { sensitiveVisible, setSensitiveVisible } = useUsageLogsContext()
const fetchingLogs = useIsFetching({ queryKey: ['logs'] }) const fetchingLogs = useIsFetching({ queryKey: ['logs'] })
const [filters, setFilters] = useState<CommonLogFilters>(() => { const searchState = useMemo<CommonLogDraft>(() => {
const { start, end } = getDefaultTimeRange() const { start, end } = getDefaultTimeRange()
return { startTime: start, endTime: end } const sourceValues = {
}) startTime: searchParams.startTime,
const [logType, setLogType] = useState<LogTypeValue>(LOG_TYPE_ALL_VALUE) endTime: searchParams.endTime,
channel: searchParams.channel,
useEffect(() => { model: searchParams.model,
const { start, end } = getDefaultTimeRange() token: searchParams.token,
setFilters({ group: searchParams.group,
username: searchParams.username,
requestId: searchParams.requestId,
upstreamRequestId: searchParams.upstreamRequestId,
type: searchParams.type,
}
const filters: CommonLogFilters = {
startTime: searchParams.startTime startTime: searchParams.startTime
? new Date(searchParams.startTime) ? new Date(searchParams.startTime)
: start, : start,
...@@ -94,16 +145,12 @@ export function CommonLogsFilterBar<TData>( ...@@ -94,16 +145,12 @@ export function CommonLogsFilterBar<TData>(
username: searchParams.username || undefined, username: searchParams.username || undefined,
requestId: searchParams.requestId || undefined, requestId: searchParams.requestId || undefined,
upstreamRequestId: searchParams.upstreamRequestId || undefined, upstreamRequestId: searchParams.upstreamRequestId || undefined,
}) }
return {
const typeArr = searchParams.type sourceKey: buildSearchSourceKey(sourceValues),
const nextLogType = filters,
Array.isArray(typeArr) && logType: getLogTypeValue(searchParams.type),
typeArr.length === 1 && }
isLogTypeValue(typeArr[0])
? typeArr[0]
: LOG_TYPE_ALL_VALUE
setLogType(nextLogType)
}, [ }, [
searchParams.startTime, searchParams.startTime,
searchParams.endTime, searchParams.endTime,
...@@ -116,12 +163,25 @@ export function CommonLogsFilterBar<TData>( ...@@ -116,12 +163,25 @@ export function CommonLogsFilterBar<TData>(
searchParams.upstreamRequestId, searchParams.upstreamRequestId,
searchParams.type, searchParams.type,
]) ])
const [draft, setDraft] = useState<CommonLogDraft>(() => searchState)
const activeDraft =
draft.sourceKey === searchState.sourceKey ? draft : searchState
const filters = activeDraft.filters
const logType = activeDraft.logType
const handleChange = useCallback( const handleChange = useCallback(
(field: keyof CommonLogFilters, value: Date | string | undefined) => { (field: keyof CommonLogFilters, value: Date | string | undefined) => {
setFilters((prev) => ({ ...prev, [field]: value })) setDraft((current) => {
const base =
current.sourceKey === searchState.sourceKey ? current : searchState
return {
sourceKey: searchState.sourceKey,
filters: { ...base.filters, [field]: value },
logType: base.logType,
}
})
}, },
[] [searchState]
) )
const handleApply = useCallback(() => { const handleApply = useCallback(() => {
...@@ -142,17 +202,23 @@ export function CommonLogsFilterBar<TData>( ...@@ -142,17 +202,23 @@ export function CommonLogsFilterBar<TData>(
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
const { start, end } = getDefaultTimeRange() const { start, end } = getDefaultTimeRange()
const resetFilters: CommonLogFilters = { startTime: start, endTime: end } const resetFilters: CommonLogFilters = { startTime: start, endTime: end }
setFilters(resetFilters) const resetSearch = {
setLogType(LOG_TYPE_ALL_VALUE) type: [LOG_TYPE_ALL_VALUE],
startTime: start.getTime(),
endTime: end.getTime(),
}
setDraft({
sourceKey: buildSearchSourceKey(resetSearch),
filters: resetFilters,
logType: LOG_TYPE_ALL_VALUE,
})
navigate({ navigate({
to: '/usage-logs/$section', to: '/usage-logs/$section',
params: { section: 'common' }, params: { section: 'common' },
search: { search: {
page: 1, page: 1,
type: [LOG_TYPE_ALL_VALUE], ...resetSearch,
startTime: start.getTime(),
endTime: end.getTime(),
}, },
}) })
queryClient.invalidateQueries({ queryKey: ['logs'] }) queryClient.invalidateQueries({ queryKey: ['logs'] })
...@@ -259,9 +325,19 @@ export function CommonLogsFilterBar<TData>( ...@@ -259,9 +325,19 @@ export function CommonLogsFilterBar<TData>(
items={logTypeItems} items={logTypeItems}
value={logType} value={logType}
onValueChange={(value) => { onValueChange={(value) => {
setLogType( const nextLogType =
value !== null && isLogTypeValue(value) ? value : LOG_TYPE_ALL_VALUE value !== null && isLogTypeValue(value) ? value : LOG_TYPE_ALL_VALUE
) setDraft((current) => {
const base =
current.sourceKey === searchState.sourceKey
? current
: searchState
return {
sourceKey: searchState.sourceKey,
filters: base.filters,
logType: nextLogType,
}
})
}} }}
> >
<SelectTrigger> <SelectTrigger>
......
...@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -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 { useState, useEffect } from 'react' import { useState, useEffect, useCallback } from 'react'
import { getTopupInfo } from '../api' import { getTopupInfo } from '../api'
import { import {
generatePresetAmounts, generatePresetAmounts,
...@@ -70,6 +70,7 @@ function parsePaymentMethods( ...@@ -70,6 +70,7 @@ function parsePaymentMethods(
name: typeof item.name === 'string' ? item.name : '', name: typeof item.name === 'string' ? item.name : '',
type, type,
color: typeof item.color === 'string' ? item.color : undefined, color: typeof item.color === 'string' ? item.color : undefined,
icon: typeof item.icon === 'string' ? item.icon : undefined,
min_topup: min_topup:
type === 'stripe' && normalizedMinTopup <= 0 type === 'stripe' && normalizedMinTopup <= 0
? stripeMinTopup ? stripeMinTopup
...@@ -166,7 +167,7 @@ export function useTopupInfo() { ...@@ -166,7 +167,7 @@ export function useTopupInfo() {
const [presetAmounts, setPresetAmounts] = useState<PresetAmount[]>([]) const [presetAmounts, setPresetAmounts] = useState<PresetAmount[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const fetchTopupInfo = async () => { const fetchTopupInfo = useCallback(async () => {
try { try {
setLoading(true) setLoading(true)
...@@ -211,11 +212,19 @@ export function useTopupInfo() { ...@@ -211,11 +212,19 @@ export function useTopupInfo() {
} finally { } finally {
setLoading(false) setLoading(false)
} }
} }, [])
useEffect(() => { useEffect(() => {
fetchTopupInfo() let cancelled = false
}, [])
queueMicrotask(() => {
if (!cancelled) void fetchTopupInfo()
})
return () => {
cancelled = true
}
}, [fetchTopupInfo])
return { return {
topupInfo, topupInfo,
......
...@@ -20,6 +20,7 @@ import { type ReactNode } from 'react' ...@@ -20,6 +20,7 @@ import { type ReactNode } from 'react'
import i18next from 'i18next' import i18next from 'i18next'
import { CreditCard, Landmark } from 'lucide-react' import { CreditCard, Landmark } from 'lucide-react'
import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si' import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si'
import { ReactIconByName } from '@/components/react-icon-by-name'
import { PAYMENT_TYPES, PAYMENT_ICON_COLORS } from '../constants' import { PAYMENT_TYPES, PAYMENT_ICON_COLORS } from '../constants'
// ============================================================================ // ============================================================================
...@@ -57,16 +58,18 @@ function normalizeHttpIconUrl(raw: string | undefined | null): string | null { ...@@ -57,16 +58,18 @@ function normalizeHttpIconUrl(raw: string | undefined | null): string | null {
/** /**
* Get payment method icon component * Get payment method icon component
* *
* When iconUrl is provided, render an <img/> with that URL so custom * When icon is provided, render a safe http(s) image URL or resolve it as a
* gateway logos can be configured per-method. * react-icons component name. Invalid configured icons intentionally render
* nothing instead of falling back to the payment type.
*/ */
export function getPaymentIcon( export function getPaymentIcon(
paymentType: string | undefined, paymentType: string | undefined,
className: string = 'h-4 w-4', className: string = 'h-4 w-4',
iconUrl?: string, icon?: string,
altName?: string altName?: string
): ReactNode { ): ReactNode {
const safeIconUrl = normalizeHttpIconUrl(iconUrl) const iconValue = icon?.trim()
const safeIconUrl = normalizeHttpIconUrl(iconValue)
if (safeIconUrl) { if (safeIconUrl) {
return ( return (
<img <img
...@@ -80,6 +83,15 @@ export function getPaymentIcon( ...@@ -80,6 +83,15 @@ export function getPaymentIcon(
/> />
) )
} }
if (iconValue) {
return (
<ReactIconByName
name={iconValue}
className={className}
title={altName || paymentType || iconValue}
/>
)
}
if (!paymentType) { if (!paymentType) {
return <CreditCard className={className} /> return <CreditCard className={className} />
......
...@@ -94,11 +94,11 @@ export interface PaymentMethod { ...@@ -94,11 +94,11 @@ export interface PaymentMethod {
name: string name: string
/** Payment method type identifier */ /** Payment method type identifier */
type: string type: string
/** Optional color for UI display */ /** Legacy optional color for UI display */
color?: string color?: string
/** Minimum topup amount for this payment method */ /** Minimum topup amount for this payment method */
min_topup?: number min_topup?: number
/** Optional icon URL provided by backend (preferred over built-in icons) */ /** Optional react-icons component name or safe icon URL */
icon?: string icon?: string
} }
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"(Override all channels' models)": "(Override all channels' models)", "(Override all channels' models)": "(Override all channels' models)",
"[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]": "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]", "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]": "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]",
"[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]", "[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]",
"[{\"name\":\"支付宝\",\"type\":\"alipay\",\"icon\":\"SiAlipay\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"icon\":\"SiAlipay\"}]",
"{\"original-model\": \"replacement-model\"}": "{\"original-model\": \"replacement-model\"}", "{\"original-model\": \"replacement-model\"}": "{\"original-model\": \"replacement-model\"}",
"{{category}} Models": "{{category}} Models", "{{category}} Models": "{{category}} Models",
"{{completed}}/{{total}} completed": "{{completed}}/{{total}} completed", "{{completed}}/{{total}} completed": "{{completed}}/{{total}} completed",
...@@ -876,6 +877,7 @@ ...@@ -876,6 +877,7 @@
"Configure Waffo payment aggregation platform integration": "Configure Waffo payment aggregation platform integration", "Configure Waffo payment aggregation platform integration": "Configure Waffo payment aggregation platform integration",
"Configure your account behavior preferences": "Configure your account behavior preferences", "Configure your account behavior preferences": "Configure your account behavior preferences",
"Configure your account preferences and integrations": "Configure your account preferences and integrations", "Configure your account preferences and integrations": "Configure your account preferences and integrations",
"Configured as PayMethods JSON. The type value decides which payment flow is used: stripe for Stripe, waffo_pancake for Waffo Pancake, and other values are sent to Epay as the type parameter.": "Configured as PayMethods JSON. The type value decides which payment flow is used: stripe for Stripe, waffo_pancake for Waffo Pancake, and other values are sent to Epay as the type parameter.",
"Configured routes and latency checks": "Configured routes and latency checks", "Configured routes and latency checks": "Configured routes and latency checks",
"Confirm": "Confirm", "Confirm": "Confirm",
"Confirm Action": "Confirm Action", "Confirm Action": "Confirm Action",
...@@ -1081,6 +1083,7 @@ ...@@ -1081,6 +1083,7 @@
"Custom Currency Symbol": "Custom Currency Symbol", "Custom Currency Symbol": "Custom Currency Symbol",
"Custom currency symbol is required": "Custom currency symbol is required", "Custom currency symbol is required": "Custom currency symbol is required",
"Custom database driver detected.": "Custom database driver detected.", "Custom database driver detected.": "Custom database driver detected.",
"Custom Epay method": "Custom Epay method",
"Custom Error Response": "Custom Error Response", "Custom Error Response": "Custom Error Response",
"Custom Home Page": "Custom Home Page", "Custom Home Page": "Custom Home Page",
"Custom message shown when access is denied": "Custom message shown when access is denied", "Custom message shown when access is denied": "Custom message shown when access is denied",
...@@ -1349,6 +1352,7 @@ ...@@ -1349,6 +1352,7 @@
"e.g., preview": "e.g., preview", "e.g., preview": "e.g., preview",
"e.g., prod_xxx": "e.g., prod_xxx", "e.g., prod_xxx": "e.g., prod_xxx",
"e.g., Recommended for China Mainland Users": "e.g., Recommended for China Mainland Users", "e.g., Recommended for China Mainland Users": "e.g., Recommended for China Mainland Users",
"e.g., SiAlipay": "e.g., SiAlipay",
"e.g., us-central1 or JSON format for model-specific regions": "e.g., us-central1 or JSON format for model-specific regions", "e.g., us-central1 or JSON format for model-specific regions": "e.g., us-central1 or JSON format for model-specific regions",
"e.g., v2.1": "e.g., v2.1", "e.g., v2.1": "e.g., v2.1",
"Each backup code can only be used once.": "Each backup code can only be used once.", "Each backup code can only be used once.": "Each backup code can only be used once.",
...@@ -1471,6 +1475,7 @@ ...@@ -1471,6 +1475,7 @@
"Enter a name": "Enter a name", "Enter a name": "Enter a name",
"Enter a new name": "Enter a new name", "Enter a new name": "Enter a new name",
"Enter a positive or negative amount to adjust the quota": "Enter a positive or negative amount to adjust the quota", "Enter a positive or negative amount to adjust the quota": "Enter a positive or negative amount to adjust the quota",
"Enter a react-icons component name. Invalid names show no icon.": "Enter a react-icons component name. Invalid names show no icon.",
"Enter a valid email or leave blank": "Enter a valid email or leave blank", "Enter a valid email or leave blank": "Enter a valid email or leave blank",
"Enter a value and press Enter": "Enter a value and press Enter", "Enter a value and press Enter": "Enter a value and press Enter",
"Enter amount in {{currency}}": "Enter amount in {{currency}}", "Enter amount in {{currency}}": "Enter amount in {{currency}}",
...@@ -1505,6 +1510,7 @@ ...@@ -1505,6 +1510,7 @@
"Enter one API key per line for batch creation": "Enter one API key per line for batch creation", "Enter one API key per line for batch creation": "Enter one API key per line for batch creation",
"Enter one key per line for batch creation": "Enter one key per line for batch creation", "Enter one key per line for batch creation": "Enter one key per line for batch creation",
"Enter one keyword per line": "Enter one keyword per line", "Enter one keyword per line": "Enter one keyword per line",
"Enter only a top-level callback domain, for example https://api.example.com, without any path.": "Enter only a top-level callback domain, for example https://api.example.com, without any path.",
"Enter password": "Enter password", "Enter password": "Enter password",
"Enter password (8-20 characters)": "Enter password (8-20 characters)", "Enter password (8-20 characters)": "Enter password (8-20 characters)",
"Enter password (min 8 characters)": "Enter password (min 8 characters)", "Enter password (min 8 characters)": "Enter password (min 8 characters)",
...@@ -1541,10 +1547,14 @@ ...@@ -1541,10 +1547,14 @@
"Env (JSON object)": "Env (JSON object)", "Env (JSON object)": "Env (JSON object)",
"Environment variables": "Environment variables", "Environment variables": "Environment variables",
"Environment variables (JSON)": "Environment variables (JSON)", "Environment variables (JSON)": "Environment variables (JSON)",
"Epay Alipay": "Epay Alipay",
"Epay endpoint": "Epay endpoint", "Epay endpoint": "Epay endpoint",
"Epay Gateway": "Epay Gateway", "Epay Gateway": "Epay Gateway",
"Epay is a payment protocol, not a specific official website. Verify the provider yourself and do not trust random third-party Epay deployments.": "Epay is a payment protocol, not a specific official website. Verify the provider yourself and do not trust random third-party Epay deployments.",
"Epay merchant ID": "Epay merchant ID", "Epay merchant ID": "Epay merchant ID",
"Epay safety reminder": "Epay safety reminder",
"Epay secret key": "Epay secret key", "Epay secret key": "Epay secret key",
"Epay WeChat Pay": "Epay WeChat Pay",
"Equals": "Equals", "Equals": "Equals",
"Error": "Error", "Error": "Error",
"Error Code (optional)": "Error Code (optional)", "Error Code (optional)": "Error Code (optional)",
...@@ -2763,6 +2773,7 @@ ...@@ -2763,6 +2773,7 @@
"Only allow specific email domains": "Only allow specific email domains", "Only allow specific email domains": "Only allow specific email domains",
"Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.", "Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.",
"Only configured combinations are overridden. All other calls keep the token group base ratio.": "Only configured combinations are overridden. All other calls keep the token group base ratio.", "Only configured combinations are overridden. All other calls keep the token group base ratio.": "Only configured combinations are overridden. All other calls keep the token group base ratio.",
"Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.": "Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.",
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.", "Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.",
"Only successful requests": "Only successful requests", "Only successful requests": "Only successful requests",
"Only successful requests count toward this limit.": "Only successful requests count toward this limit.", "Only successful requests count toward this limit.": "Only successful requests count toward this limit.",
...@@ -2921,6 +2932,8 @@ ...@@ -2921,6 +2932,8 @@
"Payment Gateway": "Payment Gateway", "Payment Gateway": "Payment Gateway",
"Payment initiated": "Payment initiated", "Payment initiated": "Payment initiated",
"Payment Method": "Payment Method", "Payment Method": "Payment Method",
"Payment method identifier": "Payment method identifier",
"Payment method identifier is required": "Payment method identifier is required",
"Payment method name": "Payment method name", "Payment method name": "Payment method name",
"Payment method name is required": "Payment method name is required", "Payment method name is required": "Payment method name is required",
"Payment method type": "Payment method type", "Payment method type": "Payment method type",
...@@ -2931,6 +2944,8 @@ ...@@ -2931,6 +2944,8 @@
"Payment request failed": "Payment request failed", "Payment request failed": "Payment request failed",
"Payment return URL": "Payment return URL", "Payment return URL": "Payment return URL",
"Payment return URL is empty. Create the product without a SuccessURL redirect?": "Payment return URL is empty. Create the product without a SuccessURL redirect?", "Payment return URL is empty. Create the product without a SuccessURL redirect?": "Payment return URL is empty. Create the product without a SuccessURL redirect?",
"Payment type key": "Payment type key",
"Payment type key is required": "Payment type key is required",
"Payment, redemption codes, subscription plans, and invitation rewards are locked until the root administrator confirms the compliance terms.": "Payment, redemption codes, subscription plans, and invitation rewards are locked until the root administrator confirms the compliance terms.", "Payment, redemption codes, subscription plans, and invitation rewards are locked until the root administrator confirms the compliance terms.": "Payment, redemption codes, subscription plans, and invitation rewards are locked until the root administrator confirms the compliance terms.",
"Peak": "Peak", "Peak": "Peak",
"Peak throughput": "Peak throughput", "Peak throughput": "Peak throughput",
...@@ -3189,6 +3204,7 @@ ...@@ -3189,6 +3204,7 @@
"Queued": "Queued", "Queued": "Queued",
"Quick actions": "Quick actions", "Quick actions": "Quick actions",
"Quick insert common payment methods": "Quick insert common payment methods", "Quick insert common payment methods": "Quick insert common payment methods",
"Quick insert payment entries": "Quick insert payment entries",
"Quick Range": "Quick Range", "Quick Range": "Quick Range",
"Quick Setup from Preset": "Quick Setup from Preset", "Quick Setup from Preset": "Quick Setup from Preset",
"Quota": "Quota", "Quota": "Quota",
...@@ -3546,6 +3562,7 @@ ...@@ -3546,6 +3562,7 @@
"Search feature in development": "Search feature in development", "Search feature in development": "Search feature in development",
"Search group names...": "Search group names...", "Search group names...": "Search group names...",
"Search groups...": "Search groups...", "Search groups...": "Search groups...",
"Search method identifiers...": "Search method identifiers...",
"Search missing models": "Search missing models", "Search missing models": "Search missing models",
"Search model name, provider, endpoint, or tag...": "Search model name, provider, endpoint, or tag...", "Search model name, provider, endpoint, or tag...": "Search model name, provider, endpoint, or tag...",
"Search model name...": "Search model name...", "Search model name...": "Search model name...",
...@@ -3553,6 +3570,7 @@ ...@@ -3553,6 +3570,7 @@
"Search models or fields...": "Search models or fields...", "Search models or fields...": "Search models or fields...",
"Search models...": "Search models...", "Search models...": "Search models...",
"Search payment methods...": "Search payment methods...", "Search payment methods...": "Search payment methods...",
"Search payment type keys...": "Search payment type keys...",
"Search payment types...": "Search payment types...", "Search payment types...": "Search payment types...",
"Search products...": "Search products...", "Search products...": "Search products...",
"Search rules...": "Search rules...", "Search rules...": "Search rules...",
...@@ -3621,8 +3639,10 @@ ...@@ -3621,8 +3639,10 @@
"Select models to process. Unselected \"add\" models will be ignored.": "Select models to process. Unselected \"add\" models will be ignored.", "Select models to process. Unselected \"add\" models will be ignored.": "Select models to process. Unselected \"add\" models will be ignored.",
"Select models to run batch tests.": "Select models to run batch tests.", "Select models to run batch tests.": "Select models to run batch tests.",
"Select or enter color value": "Select or enter color value", "Select or enter color value": "Select or enter color value",
"Select or enter method identifier": "Select or enter method identifier",
"Select or enter model name": "Select or enter model name", "Select or enter model name": "Select or enter model name",
"Select or enter payment type": "Select or enter payment type", "Select or enter payment type": "Select or enter payment type",
"Select or enter payment type key": "Select or enter payment type key",
"Select payment method": "Select payment method", "Select payment method": "Select payment method",
"Select preset or enter custom CSS color value.": "Select preset or enter custom CSS color value.", "Select preset or enter custom CSS color value.": "Select preset or enter custom CSS color value.",
"Select publish date": "Select publish date", "Select publish date": "Select publish date",
...@@ -4021,6 +4041,7 @@ ...@@ -4021,6 +4041,7 @@
"This expression is too complex for the visual editor. Please switch to expression mode to edit.": "This expression is too complex for the visual editor. Please switch to expression mode to edit.", "This expression is too complex for the visual editor. Please switch to expression mode to edit.": "This expression is too complex for the visual editor. Please switch to expression mode to edit.",
"This feature is experimental. Configuration format and behavior may change.": "This feature is experimental. Configuration format and behavior may change.", "This feature is experimental. Configuration format and behavior may change.": "This feature is experimental. Configuration format and behavior may change.",
"This feature requires server-side WeChat configuration": "This feature requires server-side WeChat configuration", "This feature requires server-side WeChat configuration": "This feature requires server-side WeChat configuration",
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.",
"This may cause cache failures.": "This may cause cache failures.", "This may cause cache failures.": "This may cause cache failures.",
"This may take a few moments while we validate the request and update your session.": "This may take a few moments while we validate the request and update your session.", "This may take a few moments while we validate the request and update your session.": "This may take a few moments while we validate the request and update your session.",
"This model has both fixed price and ratio billing conflicts": "This model has both fixed price and ratio billing conflicts", "This model has both fixed price and ratio billing conflicts": "This model has both fixed price and ratio billing conflicts",
...@@ -4357,6 +4378,7 @@ ...@@ -4357,6 +4378,7 @@
"Used Quota": "Used Quota", "Used Quota": "Used Quota",
"Used to authenticate with io.net deployment API": "Used to authenticate with io.net deployment API", "Used to authenticate with io.net deployment API": "Used to authenticate with io.net deployment API",
"Used to authenticate with the worker. Leave blank to keep the existing secret.": "Used to authenticate with the worker. Leave blank to keep the existing secret.", "Used to authenticate with the worker. Leave blank to keep the existing secret.": "Used to authenticate with the worker. Leave blank to keep the existing secret.",
"Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.": "Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.",
"Used:": "Used:", "Used:": "Used:",
"User": "User", "User": "User",
"User {{id}}": "User {{id}}", "User {{id}}": "User {{id}}",
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
"_copy": "_copie", "_copy": "_copie",
",": ", ", ",": ", ",
", and": ", et", ", and": ", et",
",and ": ", and ", ",and ": " et ",
"、": ", ", "、": ", ",
"? This action cannot be undone.": "? Cette action ne peut pas être annulée.", "? This action cannot be undone.": "? Cette action ne peut pas être annulée.",
". Please fix the JSON before saving.": ". Veuillez corriger le JSON avant d'enregistrer.", ". Please fix the JSON before saving.": ". Veuillez corriger le JSON avant d'enregistrer.",
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"(Override all channels' models)": "(Remplacer les modèles de tous les canaux)", "(Override all channels' models)": "(Remplacer les modèles de tous les canaux)",
"[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]": "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]", "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]": "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]",
"[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]", "[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]",
"[{\"name\":\"支付宝\",\"type\":\"alipay\",\"icon\":\"SiAlipay\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"icon\":\"SiAlipay\"}]",
"{\"original-model\": \"replacement-model\"}": "{\"original-model\": \"replacement-model\"}", "{\"original-model\": \"replacement-model\"}": "{\"original-model\": \"replacement-model\"}",
"{{category}} Models": "Modèles {{category}}", "{{category}} Models": "Modèles {{category}}",
"{{completed}}/{{total}} completed": "{{completed}}/{{total}} terminé(s)", "{{completed}}/{{total}} completed": "{{completed}}/{{total}} terminé(s)",
...@@ -876,6 +877,7 @@ ...@@ -876,6 +877,7 @@
"Configure Waffo payment aggregation platform integration": "Configurer l'intégration de la plateforme d'agrégation de paiement Waffo", "Configure Waffo payment aggregation platform integration": "Configurer l'intégration de la plateforme d'agrégation de paiement Waffo",
"Configure your account behavior preferences": "Configurer les préférences de comportement de votre compte", "Configure your account behavior preferences": "Configurer les préférences de comportement de votre compte",
"Configure your account preferences and integrations": "Configurer les préférences et les intégrations de votre compte", "Configure your account preferences and integrations": "Configurer les préférences et les intégrations de votre compte",
"Configured as PayMethods JSON. The type value decides which payment flow is used: stripe for Stripe, waffo_pancake for Waffo Pancake, and other values are sent to Epay as the type parameter.": "Enregistré comme JSON PayMethods. La valeur type décide du flux de paiement utilisé : stripe pour Stripe, waffo_pancake pour Waffo Pancake, et les autres valeurs sont envoyées à Epay comme paramètre type.",
"Configured routes and latency checks": "Routes configurées et contrôles de latence", "Configured routes and latency checks": "Routes configurées et contrôles de latence",
"Confirm": "Confirmer", "Confirm": "Confirmer",
"Confirm Action": "Confirmer l'action", "Confirm Action": "Confirmer l'action",
...@@ -1081,6 +1083,7 @@ ...@@ -1081,6 +1083,7 @@
"Custom Currency Symbol": "Symbole de devise personnalisé", "Custom Currency Symbol": "Symbole de devise personnalisé",
"Custom currency symbol is required": "Le symbole de devise personnalisé est requis", "Custom currency symbol is required": "Le symbole de devise personnalisé est requis",
"Custom database driver detected.": "Pilote de base de données personnalisé détecté.", "Custom database driver detected.": "Pilote de base de données personnalisé détecté.",
"Custom Epay method": "Méthode Epay personnalisée",
"Custom Error Response": "Réponse d'erreur personnalisée", "Custom Error Response": "Réponse d'erreur personnalisée",
"Custom Home Page": "Page d'accueil personnalisée", "Custom Home Page": "Page d'accueil personnalisée",
"Custom message shown when access is denied": "Message personnalisé affiché lorsque l'accès est refusé", "Custom message shown when access is denied": "Message personnalisé affiché lorsque l'accès est refusé",
...@@ -1349,6 +1352,7 @@ ...@@ -1349,6 +1352,7 @@
"e.g., preview": "par ex., prévisualisation", "e.g., preview": "par ex., prévisualisation",
"e.g., prod_xxx": "p. ex., prod_xxx", "e.g., prod_xxx": "p. ex., prod_xxx",
"e.g., Recommended for China Mainland Users": "par ex., Recommandé pour les utilisateurs de Chine continentale", "e.g., Recommended for China Mainland Users": "par ex., Recommandé pour les utilisateurs de Chine continentale",
"e.g., SiAlipay": "par ex., SiAlipay",
"e.g., us-central1 or JSON format for model-specific regions": "par ex., us-central1 ou format JSON pour les régions spécifiques au modèle", "e.g., us-central1 or JSON format for model-specific regions": "par ex., us-central1 ou format JSON pour les régions spécifiques au modèle",
"e.g., v2.1": "par ex., v2.1", "e.g., v2.1": "par ex., v2.1",
"Each backup code can only be used once.": "Chaque code de sauvegarde ne peut être utilisé qu'une seule fois.", "Each backup code can only be used once.": "Chaque code de sauvegarde ne peut être utilisé qu'une seule fois.",
...@@ -1471,6 +1475,7 @@ ...@@ -1471,6 +1475,7 @@
"Enter a name": "Saisir un nom", "Enter a name": "Saisir un nom",
"Enter a new name": "Entrez un nouveau nom", "Enter a new name": "Entrez un nouveau nom",
"Enter a positive or negative amount to adjust the quota": "Saisir un montant positif ou négatif pour ajuster le quota", "Enter a positive or negative amount to adjust the quota": "Saisir un montant positif ou négatif pour ajuster le quota",
"Enter a react-icons component name. Invalid names show no icon.": "Saisissez le nom d’un composant react-icons. Les noms invalides n’affichent aucune icône.",
"Enter a valid email or leave blank": "Entrez un e-mail valide ou laissez vide", "Enter a valid email or leave blank": "Entrez un e-mail valide ou laissez vide",
"Enter a value and press Enter": "Saisir une valeur et appuyer sur Entrée", "Enter a value and press Enter": "Saisir une valeur et appuyer sur Entrée",
"Enter amount in {{currency}}": "Entrez le montant en {{currency}}", "Enter amount in {{currency}}": "Entrez le montant en {{currency}}",
...@@ -1505,6 +1510,7 @@ ...@@ -1505,6 +1510,7 @@
"Enter one API key per line for batch creation": "Saisissez une clé API par ligne pour la création par lots", "Enter one API key per line for batch creation": "Saisissez une clé API par ligne pour la création par lots",
"Enter one key per line for batch creation": "Saisissez une clé par ligne pour la création par lots", "Enter one key per line for batch creation": "Saisissez une clé par ligne pour la création par lots",
"Enter one keyword per line": "Saisir un mot-clé par ligne", "Enter one keyword per line": "Saisir un mot-clé par ligne",
"Enter only a top-level callback domain, for example https://api.example.com, without any path.": "Saisissez uniquement le domaine de callback principal, par exemple https://api.example.com, sans chemin.",
"Enter password": "Saisir le mot de passe", "Enter password": "Saisir le mot de passe",
"Enter password (8-20 characters)": "Saisir le mot de passe (8-20 caractères)", "Enter password (8-20 characters)": "Saisir le mot de passe (8-20 caractères)",
"Enter password (min 8 characters)": "Entrez le mot de passe (min. 8 caractères)", "Enter password (min 8 characters)": "Entrez le mot de passe (min. 8 caractères)",
...@@ -1541,10 +1547,14 @@ ...@@ -1541,10 +1547,14 @@
"Env (JSON object)": "Env (objet JSON)", "Env (JSON object)": "Env (objet JSON)",
"Environment variables": "Variables d'environnement", "Environment variables": "Variables d'environnement",
"Environment variables (JSON)": "Variables d'environnement (JSON)", "Environment variables (JSON)": "Variables d'environnement (JSON)",
"Epay Alipay": "Alipay via Epay",
"Epay endpoint": "Endpoint Epay", "Epay endpoint": "Endpoint Epay",
"Epay Gateway": "Passerelle Epay", "Epay Gateway": "Passerelle Epay",
"Epay is a payment protocol, not a specific official website. Verify the provider yourself and do not trust random third-party Epay deployments.": "Epay est un protocole de paiement, pas un site officiel précis. Vérifiez vous-même le fournisseur et ne faites pas confiance à des déploiements Epay tiers aléatoires.",
"Epay merchant ID": "ID marchand Epay", "Epay merchant ID": "ID marchand Epay",
"Epay safety reminder": "Rappel de sécurité Epay",
"Epay secret key": "Clé secrète Epay", "Epay secret key": "Clé secrète Epay",
"Epay WeChat Pay": "WeChat Pay via Epay",
"Equals": "Égal à", "Equals": "Égal à",
"Error": "Erreur", "Error": "Erreur",
"Error Code (optional)": "Code d'erreur (optionnel)", "Error Code (optional)": "Code d'erreur (optionnel)",
...@@ -2763,6 +2773,7 @@ ...@@ -2763,6 +2773,7 @@
"Only allow specific email domains": "Autoriser uniquement des domaines d'e-mail spécifiques", "Only allow specific email domains": "Autoriser uniquement des domaines d'e-mail spécifiques",
"Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "Uniquement disponible pour les administrateurs. Lorsque cette option est activée, vous recevrez une notification récapitulative via votre méthode sélectionnée lorsque la vérification planifiée des modèles détecte des changements de modèles en amont ou des échecs de vérification.", "Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "Uniquement disponible pour les administrateurs. Lorsque cette option est activée, vous recevrez une notification récapitulative via votre méthode sélectionnée lorsque la vérification planifiée des modèles détecte des changements de modèles en amont ou des échecs de vérification.",
"Only configured combinations are overridden. All other calls keep the token group base ratio.": "Seules les combinaisons configurées sont remplacées. Les autres appels conservent le ratio de base du groupe du jeton.", "Only configured combinations are overridden. All other calls keep the token group base ratio.": "Seules les combinaisons configurées sont remplacées. Les autres appels conservent le ratio de base du groupe du jeton.",
"Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.": "Saisissez uniquement l’origine du site, par exemple https://api.example.com. N’ajoutez aucun chemin comme /api/user/epay/notify. Laissez vide pour utiliser l’adresse du serveur.",
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Seuls les champs sélectionnés seront écrasés. Vous pouvez relancer l'assistant de synchronisation si de nouveaux conflits apparaissent.", "Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Seuls les champs sélectionnés seront écrasés. Vous pouvez relancer l'assistant de synchronisation si de nouveaux conflits apparaissent.",
"Only successful requests": "Uniquement les requêtes réussies", "Only successful requests": "Uniquement les requêtes réussies",
"Only successful requests count toward this limit.": "Seules les requêtes réussies comptent pour cette limite.", "Only successful requests count toward this limit.": "Seules les requêtes réussies comptent pour cette limite.",
...@@ -2792,7 +2803,7 @@ ...@@ -2792,7 +2803,7 @@
"OpenRouter": "OpenRouter", "OpenRouter": "OpenRouter",
"opens in an external client. Trigger it from the sidebar or API key actions to launch the configured application.": "s'ouvre dans un client externe. Déclenchez-le depuis la barre latérale ou les actions de clé API pour lancer l'application configurée.", "opens in an external client. Trigger it from the sidebar or API key actions to launch the configured application.": "s'ouvre dans un client externe. Déclenchez-le depuis la barre latérale ou les actions de clé API pour lancer l'application configurée.",
"Operation": "Opération", "Operation": "Opération",
"operation and charging behavior": "exploitation et facturation", "operation and charging behavior": "à l’exploitation et à la facturation",
"Operation Audit Info": "Informations d'audit d'opération", "Operation Audit Info": "Informations d'audit d'opération",
"Operation failed": "Opération échouée", "Operation failed": "Opération échouée",
"Operation successful": "Opération réussie", "Operation successful": "Opération réussie",
...@@ -2921,6 +2932,8 @@ ...@@ -2921,6 +2932,8 @@
"Payment Gateway": "Passerelle de paiement", "Payment Gateway": "Passerelle de paiement",
"Payment initiated": "Paiement initié", "Payment initiated": "Paiement initié",
"Payment Method": "Méthode de paiement", "Payment Method": "Méthode de paiement",
"Payment method identifier": "Identifiant du mode de paiement",
"Payment method identifier is required": "L’identifiant du mode de paiement est requis",
"Payment method name": "Nom du mode de paiement", "Payment method name": "Nom du mode de paiement",
"Payment method name is required": "Le nom du mode de paiement est requis", "Payment method name is required": "Le nom du mode de paiement est requis",
"Payment method type": "Type de mode de paiement", "Payment method type": "Type de mode de paiement",
...@@ -2931,6 +2944,8 @@ ...@@ -2931,6 +2944,8 @@
"Payment request failed": "Requête de paiement échouée", "Payment request failed": "Requête de paiement échouée",
"Payment return URL": "URL de retour de paiement", "Payment return URL": "URL de retour de paiement",
"Payment return URL is empty. Create the product without a SuccessURL redirect?": "L’URL de retour de paiement est vide. Créer le produit sans redirection SuccessURL ?", "Payment return URL is empty. Create the product without a SuccessURL redirect?": "L’URL de retour de paiement est vide. Créer le produit sans redirection SuccessURL ?",
"Payment type key": "Clé de type de paiement",
"Payment type key is required": "La clé de type de paiement est requise",
"Payment, redemption codes, subscription plans, and invitation rewards are locked until the root administrator confirms the compliance terms.": "Les paiements, codes de兑换, forfaits d’abonnement et récompenses d’invitation sont verrouillés jusqu’à ce que l’administrateur racine confirme les conditions de conformité.", "Payment, redemption codes, subscription plans, and invitation rewards are locked until the root administrator confirms the compliance terms.": "Les paiements, codes de兑换, forfaits d’abonnement et récompenses d’invitation sont verrouillés jusqu’à ce que l’administrateur racine confirme les conditions de conformité.",
"Peak": "Pic", "Peak": "Pic",
"Peak throughput": "Débit de pointe", "Peak throughput": "Débit de pointe",
...@@ -3189,6 +3204,7 @@ ...@@ -3189,6 +3204,7 @@
"Queued": "En file", "Queued": "En file",
"Quick actions": "Actions rapides", "Quick actions": "Actions rapides",
"Quick insert common payment methods": "Insérer rapidement les méthodes de paiement courantes", "Quick insert common payment methods": "Insérer rapidement les méthodes de paiement courantes",
"Quick insert payment entries": "Insérer rapidement des entrées de paiement",
"Quick Range": "Plage rapide", "Quick Range": "Plage rapide",
"Quick Setup from Preset": "Configuration rapide à partir d'un préréglage", "Quick Setup from Preset": "Configuration rapide à partir d'un préréglage",
"Quota": "Quota", "Quota": "Quota",
...@@ -3546,6 +3562,7 @@ ...@@ -3546,6 +3562,7 @@
"Search feature in development": "Fonctionnalité de recherche en développement", "Search feature in development": "Fonctionnalité de recherche en développement",
"Search group names...": "Rechercher des noms de groupe...", "Search group names...": "Rechercher des noms de groupe...",
"Search groups...": "Rechercher des groupes...", "Search groups...": "Rechercher des groupes...",
"Search method identifiers...": "Rechercher des identifiants de modes...",
"Search missing models": "Rechercher les modèles manquants", "Search missing models": "Rechercher les modèles manquants",
"Search model name, provider, endpoint, or tag...": "Rechercher un nom de modèle, fournisseur, endpoint ou tag...", "Search model name, provider, endpoint, or tag...": "Rechercher un nom de modèle, fournisseur, endpoint ou tag...",
"Search model name...": "Rechercher le nom du modèle...", "Search model name...": "Rechercher le nom du modèle...",
...@@ -3553,6 +3570,7 @@ ...@@ -3553,6 +3570,7 @@
"Search models or fields...": "Rechercher des modèles ou des champs...", "Search models or fields...": "Rechercher des modèles ou des champs...",
"Search models...": "Rechercher des modèles...", "Search models...": "Rechercher des modèles...",
"Search payment methods...": "Rechercher des méthodes de paiement...", "Search payment methods...": "Rechercher des méthodes de paiement...",
"Search payment type keys...": "Rechercher des clés de type de paiement...",
"Search payment types...": "Rechercher des types de paiement...", "Search payment types...": "Rechercher des types de paiement...",
"Search products...": "Rechercher des produits...", "Search products...": "Rechercher des produits...",
"Search rules...": "Rechercher des règles…", "Search rules...": "Rechercher des règles…",
...@@ -3621,8 +3639,10 @@ ...@@ -3621,8 +3639,10 @@
"Select models to process. Unselected \"add\" models will be ignored.": "Sélectionnez les modèles à traiter. Les modèles « ajout » non sélectionnés seront ignorés.", "Select models to process. Unselected \"add\" models will be ignored.": "Sélectionnez les modèles à traiter. Les modèles « ajout » non sélectionnés seront ignorés.",
"Select models to run batch tests.": "Sélectionner les modèles pour exécuter les tests par lots.", "Select models to run batch tests.": "Sélectionner les modèles pour exécuter les tests par lots.",
"Select or enter color value": "Sélectionner ou saisir une valeur de couleur", "Select or enter color value": "Sélectionner ou saisir une valeur de couleur",
"Select or enter method identifier": "Sélectionner ou saisir l’identifiant du mode",
"Select or enter model name": "Sélectionner ou saisir le nom du modèle", "Select or enter model name": "Sélectionner ou saisir le nom du modèle",
"Select or enter payment type": "Sélectionner ou saisir le type de paiement", "Select or enter payment type": "Sélectionner ou saisir le type de paiement",
"Select or enter payment type key": "Sélectionner ou saisir la clé de type de paiement",
"Select payment method": "Sélectionner le mode de paiement", "Select payment method": "Sélectionner le mode de paiement",
"Select preset or enter custom CSS color value.": "Sélectionner un préréglage ou saisir une valeur de couleur CSS personnalisée.", "Select preset or enter custom CSS color value.": "Sélectionner un préréglage ou saisir une valeur de couleur CSS personnalisée.",
"Select publish date": "Sélectionner la date de publication", "Select publish date": "Sélectionner la date de publication",
...@@ -4021,6 +4041,7 @@ ...@@ -4021,6 +4041,7 @@
"This expression is too complex for the visual editor. Please switch to expression mode to edit.": "Cette expression est trop complexe pour l'éditeur visuel. Passez en mode expression pour la modifier.", "This expression is too complex for the visual editor. Please switch to expression mode to edit.": "Cette expression est trop complexe pour l'éditeur visuel. Passez en mode expression pour la modifier.",
"This feature is experimental. Configuration format and behavior may change.": "Cette fonctionnalité est expérimentale. Le format de configuration et le comportement peuvent changer.", "This feature is experimental. Configuration format and behavior may change.": "Cette fonctionnalité est expérimentale. Le format de configuration et le comportement peuvent changer.",
"This feature requires server-side WeChat configuration": "Cette fonctionnalité nécessite une configuration WeChat côté serveur", "This feature requires server-side WeChat configuration": "Cette fonctionnalité nécessite une configuration WeChat côté serveur",
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "Cet identifiant est envoyé au backend de paiement lors de la création d’une commande. Utilisez alipay pour Alipay, wxpay pour WeChat Pay, stripe pour Stripe. Les valeurs personnalisées doivent être prises en charge par votre fournisseur de paiement.",
"This may cause cache failures.": "Cela peut provoquer des échecs de cache.", "This may cause cache failures.": "Cela peut provoquer des échecs de cache.",
"This may take a few moments while we validate the request and update your session.": "Cela peut prendre quelques instants pendant que nous validons la requête et mettons à jour votre session.", "This may take a few moments while we validate the request and update your session.": "Cela peut prendre quelques instants pendant que nous validons la requête et mettons à jour votre session.",
"This model has both fixed price and ratio billing conflicts": "Ce modèle présente des conflits de facturation à la fois en prix fixe et au ratio", "This model has both fixed price and ratio billing conflicts": "Ce modèle présente des conflits de facturation à la fois en prix fixe et au ratio",
...@@ -4357,6 +4378,7 @@ ...@@ -4357,6 +4378,7 @@
"Used Quota": "Quota utilisé", "Used Quota": "Quota utilisé",
"Used to authenticate with io.net deployment API": "Utilisée pour l'authentification auprès de l'API de déploiement io.net", "Used to authenticate with io.net deployment API": "Utilisée pour l'authentification auprès de l'API de déploiement io.net",
"Used to authenticate with the worker. Leave blank to keep the existing secret.": "Utilisé pour l'authentification auprès du worker. Laissez vide pour conserver le secret existant.", "Used to authenticate with the worker. Leave blank to keep the existing secret.": "Utilisé pour l'authentification auprès du worker. Laissez vide pour conserver le secret existant.",
"Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.": "Détermine le flux de paiement utilisé au clic. Les clés intégrées incluent stripe pour Stripe et waffo_pancake pour Waffo Pancake ; les autres valeurs sont envoyées à Epay comme paramètre type.",
"Used:": "Utilisé :", "Used:": "Utilisé :",
"User": "Utilisateurs", "User": "Utilisateurs",
"User {{id}}": "Utilisateur {{id}}", "User {{id}}": "Utilisateur {{id}}",
......
...@@ -4,10 +4,10 @@ ...@@ -4,10 +4,10 @@
"1000": "1000", "1000": "1000",
"10000": "10000", "10000": "10000",
"_copy": "_copy", "_copy": "_copy",
",": ", ", ",": "",
", and": "、および", ", and": "、および",
",and ": ", and ", ",and ": "",
"、": ", ", "、": "",
"? This action cannot be undone.": "? この操作は元に戻せません。", "? This action cannot be undone.": "? この操作は元に戻せません。",
". Please fix the JSON before saving.": "。保存する前にJSONを修正してください。", ". Please fix the JSON before saving.": "。保存する前にJSONを修正してください。",
". This action cannot be undone.": "。この操作は元に戻せません。", ". This action cannot be undone.": "。この操作は元に戻せません。",
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"(Override all channels' models)": "(全チャネルのモデルを上書き)", "(Override all channels' models)": "(全チャネルのモデルを上書き)",
"[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]": "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]", "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]": "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]",
"[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]", "[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]",
"[{\"name\":\"支付宝\",\"type\":\"alipay\",\"icon\":\"SiAlipay\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"icon\":\"SiAlipay\"}]",
"{\"original-model\": \"replacement-model\"}": "{\" original - model \":\" replacement - model \"}", "{\"original-model\": \"replacement-model\"}": "{\" original - model \":\" replacement - model \"}",
"{{category}} Models": "{{category}} モデル", "{{category}} Models": "{{category}} モデル",
"{{completed}}/{{total}} completed": "{{completed}}/{{total}} 完了", "{{completed}}/{{total}} completed": "{{completed}}/{{total}} 完了",
...@@ -876,6 +877,7 @@ ...@@ -876,6 +877,7 @@
"Configure Waffo payment aggregation platform integration": "Waffo決済アグリゲーションプラットフォームの連携を設定", "Configure Waffo payment aggregation platform integration": "Waffo決済アグリゲーションプラットフォームの連携を設定",
"Configure your account behavior preferences": "アカウントの動作設定を設定します。", "Configure your account behavior preferences": "アカウントの動作設定を設定します。",
"Configure your account preferences and integrations": "アカウントの設定と統合を設定します。", "Configure your account preferences and integrations": "アカウントの設定と統合を設定します。",
"Configured as PayMethods JSON. The type value decides which payment flow is used: stripe for Stripe, waffo_pancake for Waffo Pancake, and other values are sent to Epay as the type parameter.": "PayMethods JSON として保存されます。type 値で使用する決済フローを決定します。stripe は Stripe、waffo_pancake は Waffo Pancake、それ以外の値は Epay の type パラメーターとして送信されます。",
"Configured routes and latency checks": "設定済みルートとレイテンシ確認", "Configured routes and latency checks": "設定済みルートとレイテンシ確認",
"Confirm": "確認", "Confirm": "確認",
"Confirm Action": "アクションの確認", "Confirm Action": "アクションの確認",
...@@ -901,7 +903,7 @@ ...@@ -901,7 +903,7 @@
"Confirm Payment": "支払いの確認", "Confirm Payment": "支払いの確認",
"Confirm Selection": "選択の確認", "Confirm Selection": "選択の確認",
"Confirm settings and finish setup": "設定を確認してセットアップを完了", "Confirm settings and finish setup": "設定を確認してセットアップを完了",
"confirm that I bear legal responsibility arising from deployment": "デプロイに起因する法的責任を負うことを確認します", "confirm that I bear legal responsibility arising from deployment": "デプロイ",
"Confirm Unbind": "連携解除を確認", "Confirm Unbind": "連携解除を確認",
"Confirm your identity before removing this Passkey from your account.": "このパスキーをアカウントから削除する前に、本人確認を行ってください。", "Confirm your identity before removing this Passkey from your account.": "このパスキーをアカウントから削除する前に、本人確認を行ってください。",
"Confirm your identity with Two-factor Authentication before registering a Passkey.": "パスキーを登録する前に、二要素認証で本人確認を行ってください。", "Confirm your identity with Two-factor Authentication before registering a Passkey.": "パスキーを登録する前に、二要素認証で本人確認を行ってください。",
...@@ -1081,6 +1083,7 @@ ...@@ -1081,6 +1083,7 @@
"Custom Currency Symbol": "カスタム通貨記号", "Custom Currency Symbol": "カスタム通貨記号",
"Custom currency symbol is required": "カスタム通貨記号は必須です", "Custom currency symbol is required": "カスタム通貨記号は必須です",
"Custom database driver detected.": "カスタムデータベースドライバーが検出されました。", "Custom database driver detected.": "カスタムデータベースドライバーが検出されました。",
"Custom Epay method": "カスタム Epay 方法",
"Custom Error Response": "カスタムエラーレスポンス", "Custom Error Response": "カスタムエラーレスポンス",
"Custom Home Page": "カスタムホームページ", "Custom Home Page": "カスタムホームページ",
"Custom message shown when access is denied": "アクセス拒否時に表示されるカスタムメッセージ", "Custom message shown when access is denied": "アクセス拒否時に表示されるカスタムメッセージ",
...@@ -1349,6 +1352,7 @@ ...@@ -1349,6 +1352,7 @@
"e.g., preview": "例: preview", "e.g., preview": "例: preview",
"e.g., prod_xxx": "例: prod_xxx", "e.g., prod_xxx": "例: prod_xxx",
"e.g., Recommended for China Mainland Users": "例: 中国本土ユーザーにおすすめ", "e.g., Recommended for China Mainland Users": "例: 中国本土ユーザーにおすすめ",
"e.g., SiAlipay": "例: SiAlipay",
"e.g., us-central1 or JSON format for model-specific regions": "例: us-central1 またはモデル固有のリージョンを示す JSON 形式", "e.g., us-central1 or JSON format for model-specific regions": "例: us-central1 またはモデル固有のリージョンを示す JSON 形式",
"e.g., v2.1": "例: v2.1", "e.g., v2.1": "例: v2.1",
"Each backup code can only be used once.": "各バックアップコードは1回しか使用できません。", "Each backup code can only be used once.": "各バックアップコードは1回しか使用できません。",
...@@ -1471,6 +1475,7 @@ ...@@ -1471,6 +1475,7 @@
"Enter a name": "名前を入力", "Enter a name": "名前を入力",
"Enter a new name": "新しい名前を入力してください", "Enter a new name": "新しい名前を入力してください",
"Enter a positive or negative amount to adjust the quota": "クォータを調整するために正または負の値を入力してください", "Enter a positive or negative amount to adjust the quota": "クォータを調整するために正または負の値を入力してください",
"Enter a react-icons component name. Invalid names show no icon.": "react-icons のコンポーネント名を入力してください。無効な名前はアイコンを表示しません。",
"Enter a valid email or leave blank": "有効なメールアドレスを入力するか空白にしてください", "Enter a valid email or leave blank": "有効なメールアドレスを入力するか空白にしてください",
"Enter a value and press Enter": "値を入力してEnterを押してください", "Enter a value and press Enter": "値を入力してEnterを押してください",
"Enter amount in {{currency}}": "{{currency}}で金額を入力", "Enter amount in {{currency}}": "{{currency}}で金額を入力",
...@@ -1505,6 +1510,7 @@ ...@@ -1505,6 +1510,7 @@
"Enter one API key per line for batch creation": "一括作成のため、1行に1つのAPIキーを入力してください", "Enter one API key per line for batch creation": "一括作成のため、1行に1つのAPIキーを入力してください",
"Enter one key per line for batch creation": "一括作成のため、1行に1つのキーを入力してください", "Enter one key per line for batch creation": "一括作成のため、1行に1つのキーを入力してください",
"Enter one keyword per line": "1行に1つのキーワードを入力", "Enter one keyword per line": "1行に1つのキーワードを入力",
"Enter only a top-level callback domain, for example https://api.example.com, without any path.": "コールバックのトップレベルドメインのみを入力してください。例: https://api.example.com。パスは含めないでください。",
"Enter password": "パスワードを入力", "Enter password": "パスワードを入力",
"Enter password (8-20 characters)": "パスワードを入力 (8~20文字)", "Enter password (8-20 characters)": "パスワードを入力 (8~20文字)",
"Enter password (min 8 characters)": "パスワードを入力(最小8文字)", "Enter password (min 8 characters)": "パスワードを入力(最小8文字)",
...@@ -1541,10 +1547,14 @@ ...@@ -1541,10 +1547,14 @@
"Env (JSON object)": "Env (JSON オブジェクト)", "Env (JSON object)": "Env (JSON オブジェクト)",
"Environment variables": "環境変数", "Environment variables": "環境変数",
"Environment variables (JSON)": "環境変数(JSON)", "Environment variables (JSON)": "環境変数(JSON)",
"Epay Alipay": "Epay Alipay(支付宝)",
"Epay endpoint": "Epayエンドポイント", "Epay endpoint": "Epayエンドポイント",
"Epay Gateway": "Epayゲートウェイ", "Epay Gateway": "Epayゲートウェイ",
"Epay is a payment protocol, not a specific official website. Verify the provider yourself and do not trust random third-party Epay deployments.": "Epay は決済プロトコルであり、特定の公式サイトではありません。提供元を自分で確認し、第三者が構築した Epay サイトを安易に信用しないでください。",
"Epay merchant ID": "Epay マーチャントID", "Epay merchant ID": "Epay マーチャントID",
"Epay safety reminder": "Epay の安全に関する注意",
"Epay secret key": "Epayシークレットキー", "Epay secret key": "Epayシークレットキー",
"Epay WeChat Pay": "Epay WeChat Pay(微信支付)",
"Equals": "等しい", "Equals": "等しい",
"Error": "エラー", "Error": "エラー",
"Error Code (optional)": "エラーコード(任意)", "Error Code (optional)": "エラーコード(任意)",
...@@ -2763,6 +2773,7 @@ ...@@ -2763,6 +2773,7 @@
"Only allow specific email domains": "特定のEメール ドメインのみを許可する", "Only allow specific email domains": "特定のEメール ドメインのみを許可する",
"Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "管理者のみ利用可能です。有効にすると、スケジュールされたモデルチェックでアップストリームモデルの変更やチェック失敗が検出された際に、選択した方法で概要通知を受け取ります。", "Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "管理者のみ利用可能です。有効にすると、スケジュールされたモデルチェックでアップストリームモデルの変更やチェック失敗が検出された際に、選択した方法で概要通知を受け取ります。",
"Only configured combinations are overridden. All other calls keep the token group base ratio.": "設定済みの組み合わせだけが上書きされます。他の呼び出しはトークングループの基本倍率を維持します。", "Only configured combinations are overridden. All other calls keep the token group base ratio.": "設定済みの組み合わせだけが上書きされます。他の呼び出しはトークングループの基本倍率を維持します。",
"Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.": "サイトのオリジンのみを入力してください。例: https://api.example.com。/api/user/epay/notify などのパスは含めないでください。空欄の場合はサーバーアドレスを使用します。",
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "選択されたフィールドのみが上書きされます。新しい競合が発生した場合は、同期ウィザードを再実行できます。", "Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "選択されたフィールドのみが上書きされます。新しい競合が発生した場合は、同期ウィザードを再実行できます。",
"Only successful requests": "成功したリクエストのみ", "Only successful requests": "成功したリクエストのみ",
"Only successful requests count toward this limit.": "成功したリクエストのみがこの制限にカウントされます。", "Only successful requests count toward this limit.": "成功したリクエストのみがこの制限にカウントされます。",
...@@ -2792,7 +2803,7 @@ ...@@ -2792,7 +2803,7 @@
"OpenRouter": "OpenRouter", "OpenRouter": "OpenRouter",
"opens in an external client. Trigger it from the sidebar or API key actions to launch the configured application.": "外部クライアントで開きます。サイドバーまたはAPIキーアクションからトリガーして、設定されたアプリケーションを起動します。", "opens in an external client. Trigger it from the sidebar or API key actions to launch the configured application.": "外部クライアントで開きます。サイドバーまたはAPIキーアクションからトリガーして、設定されたアプリケーションを起動します。",
"Operation": "操作", "Operation": "操作",
"operation and charging behavior": "運用および課金行為", "operation and charging behavior": "運用および課金行為に起因する法的責任を負うことを確認します",
"Operation Audit Info": "操作監査情報", "Operation Audit Info": "操作監査情報",
"Operation failed": "操作に失敗しました", "Operation failed": "操作に失敗しました",
"Operation successful": "操作が成功しました", "Operation successful": "操作が成功しました",
...@@ -2921,6 +2932,8 @@ ...@@ -2921,6 +2932,8 @@
"Payment Gateway": "決済ゲートウェイ", "Payment Gateway": "決済ゲートウェイ",
"Payment initiated": "支払いが開始されました", "Payment initiated": "支払いが開始されました",
"Payment Method": "チャージ方法", "Payment Method": "チャージ方法",
"Payment method identifier": "決済方法の識別子",
"Payment method identifier is required": "決済方法の識別子は必須です",
"Payment method name": "決済方法名", "Payment method name": "決済方法名",
"Payment method name is required": "決済方法名は必須です", "Payment method name is required": "決済方法名は必須です",
"Payment method type": "決済方法タイプ", "Payment method type": "決済方法タイプ",
...@@ -2931,6 +2944,8 @@ ...@@ -2931,6 +2944,8 @@
"Payment request failed": "支払いリクエストが失敗しました", "Payment request failed": "支払いリクエストが失敗しました",
"Payment return URL": "決済完了後のリダイレクトURL", "Payment return URL": "決済完了後のリダイレクトURL",
"Payment return URL is empty. Create the product without a SuccessURL redirect?": "支払い戻り URL が空です。SuccessURL リダイレクトなしで商品を作成しますか?", "Payment return URL is empty. Create the product without a SuccessURL redirect?": "支払い戻り URL が空です。SuccessURL リダイレクトなしで商品を作成しますか?",
"Payment type key": "支払いタイプキー",
"Payment type key is required": "支払いタイプキーは必須です",
"Payment, redemption codes, subscription plans, and invitation rewards are locked until the root administrator confirms the compliance terms.": "ルート管理者がコンプライアンス条件を確認するまで、支払い、引換コード、サブスクリプションプラン、招待報酬はロックされます。", "Payment, redemption codes, subscription plans, and invitation rewards are locked until the root administrator confirms the compliance terms.": "ルート管理者がコンプライアンス条件を確認するまで、支払い、引換コード、サブスクリプションプラン、招待報酬はロックされます。",
"Peak": "ピーク", "Peak": "ピーク",
"Peak throughput": "ピークスループット", "Peak throughput": "ピークスループット",
...@@ -3189,6 +3204,7 @@ ...@@ -3189,6 +3204,7 @@
"Queued": "キュー中", "Queued": "キュー中",
"Quick actions": "クイック操作", "Quick actions": "クイック操作",
"Quick insert common payment methods": "一般的な支払い方法をすばやく挿入", "Quick insert common payment methods": "一般的な支払い方法をすばやく挿入",
"Quick insert payment entries": "支払い項目をすばやく挿入",
"Quick Range": "クイック範囲", "Quick Range": "クイック範囲",
"Quick Setup from Preset": "プリセットからクイックセットアップ", "Quick Setup from Preset": "プリセットからクイックセットアップ",
"Quota": "クォータ", "Quota": "クォータ",
...@@ -3546,6 +3562,7 @@ ...@@ -3546,6 +3562,7 @@
"Search feature in development": "検索機能は開発中です", "Search feature in development": "検索機能は開発中です",
"Search group names...": "グループ名を検索...", "Search group names...": "グループ名を検索...",
"Search groups...": "グループを検索...", "Search groups...": "グループを検索...",
"Search method identifiers...": "決済方法の識別子を検索...",
"Search missing models": "不足しているモデルを検索", "Search missing models": "不足しているモデルを検索",
"Search model name, provider, endpoint, or tag...": "モデル名、プロバイダー、エンドポイント、タグを検索...", "Search model name, provider, endpoint, or tag...": "モデル名、プロバイダー、エンドポイント、タグを検索...",
"Search model name...": "モデル名を検索...", "Search model name...": "モデル名を検索...",
...@@ -3553,6 +3570,7 @@ ...@@ -3553,6 +3570,7 @@
"Search models or fields...": "モデルまたはフィールドを検索...", "Search models or fields...": "モデルまたはフィールドを検索...",
"Search models...": "モデル名で検索...", "Search models...": "モデル名で検索...",
"Search payment methods...": "支払い方法を検索...", "Search payment methods...": "支払い方法を検索...",
"Search payment type keys...": "支払いタイプキーを検索...",
"Search payment types...": "支払いタイプを検索...", "Search payment types...": "支払いタイプを検索...",
"Search products...": "商品を検索...", "Search products...": "商品を検索...",
"Search rules...": "ルールを検索…", "Search rules...": "ルールを検索…",
...@@ -3621,8 +3639,10 @@ ...@@ -3621,8 +3639,10 @@
"Select models to process. Unselected \"add\" models will be ignored.": "処理するモデルを選択してください。未選択の「追加」モデルは無視されます。", "Select models to process. Unselected \"add\" models will be ignored.": "処理するモデルを選択してください。未選択の「追加」モデルは無視されます。",
"Select models to run batch tests.": "バッチテストを実行するモデルを選択してください。", "Select models to run batch tests.": "バッチテストを実行するモデルを選択してください。",
"Select or enter color value": "色の値を選択または入力", "Select or enter color value": "色の値を選択または入力",
"Select or enter method identifier": "決済方法の識別子を選択または入力",
"Select or enter model name": "モデル名を選択または入力", "Select or enter model name": "モデル名を選択または入力",
"Select or enter payment type": "支払いタイプを選択または入力", "Select or enter payment type": "支払いタイプを選択または入力",
"Select or enter payment type key": "支払いタイプキーを選択または入力",
"Select payment method": "お支払い方法を選択", "Select payment method": "お支払い方法を選択",
"Select preset or enter custom CSS color value.": "プリセットを選択するか、カスタムCSSカラー値を入力してください。", "Select preset or enter custom CSS color value.": "プリセットを選択するか、カスタムCSSカラー値を入力してください。",
"Select publish date": "公開日を選択", "Select publish date": "公開日を選択",
...@@ -4021,6 +4041,7 @@ ...@@ -4021,6 +4041,7 @@
"This expression is too complex for the visual editor. Please switch to expression mode to edit.": "この式はビジュアルエディタでは扱いにくいです。式モードに切り替えて編集してください。", "This expression is too complex for the visual editor. Please switch to expression mode to edit.": "この式はビジュアルエディタでは扱いにくいです。式モードに切り替えて編集してください。",
"This feature is experimental. Configuration format and behavior may change.": "この機能は実験的です。設定フォーマットや動作は変更される可能性があります。", "This feature is experimental. Configuration format and behavior may change.": "この機能は実験的です。設定フォーマットや動作は変更される可能性があります。",
"This feature requires server-side WeChat configuration": "この機能にはサーバー側のWeChat設定が必要です", "This feature requires server-side WeChat configuration": "この機能にはサーバー側のWeChat設定が必要です",
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "注文作成時に、この識別子が決済バックエンドへ送信されます。Alipay は alipay、WeChat Pay は wxpay、Stripe は stripe を使ってください。カスタム値は決済サービス側で対応している必要があります。",
"This may cause cache failures.": "これによりキャッシュ障害が発生する可能性があります。", "This may cause cache failures.": "これによりキャッシュ障害が発生する可能性があります。",
"This may take a few moments while we validate the request and update your session.": "リクエストを検証し、セッションを更新するのに数分かかる場合があります。", "This may take a few moments while we validate the request and update your session.": "リクエストを検証し、セッションを更新するのに数分かかる場合があります。",
"This model has both fixed price and ratio billing conflicts": "このモデルには固定価格と比率請求の両方の競合があります", "This model has both fixed price and ratio billing conflicts": "このモデルには固定価格と比率請求の両方の競合があります",
...@@ -4357,6 +4378,7 @@ ...@@ -4357,6 +4378,7 @@
"Used Quota": "使用済みクォータ", "Used Quota": "使用済みクォータ",
"Used to authenticate with io.net deployment API": "io.net デプロイ API の認証に使用します", "Used to authenticate with io.net deployment API": "io.net デプロイ API の認証に使用します",
"Used to authenticate with the worker. Leave blank to keep the existing secret.": "ワーカーの認証に使用されます。既存のシークレットを保持するには、空欄のままにしてください。", "Used to authenticate with the worker. Leave blank to keep the existing secret.": "ワーカーの認証に使用されます。既存のシークレットを保持するには、空欄のままにしてください。",
"Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.": "クリック後に使用する決済フローを決定します。組み込みキーは Stripe 用の stripe、Waffo Pancake 用の waffo_pancake です。それ以外の値は Epay の type パラメーターとして送信されます。",
"Used:": "使用済み:", "Used:": "使用済み:",
"User": "ユーザー", "User": "ユーザー",
"User {{id}}": "ユーザー {{id}}", "User {{id}}": "ユーザー {{id}}",
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
"_copy": "_копировать", "_copy": "_копировать",
",": ", ", ",": ", ",
", and": ", и", ", and": ", и",
",and ": ", and ", ",and ": " и ",
"、": ", ", "、": ", ",
"? This action cannot be undone.": "? Это действие невозможно отменить.", "? This action cannot be undone.": "? Это действие невозможно отменить.",
". Please fix the JSON before saving.": ". Пожалуйста, исправьте JSON перед сохранением.", ". Please fix the JSON before saving.": ". Пожалуйста, исправьте JSON перед сохранением.",
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"(Override all channels' models)": "(Переопределить модели всех каналов)", "(Override all channels' models)": "(Переопределить модели всех каналов)",
"[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]": "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]", "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]": "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]",
"[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]", "[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]",
"[{\"name\":\"支付宝\",\"type\":\"alipay\",\"icon\":\"SiAlipay\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"icon\":\"SiAlipay\"}]",
"{\"original-model\": \"replacement-model\"}": "{\"original-model\": \"replacement-model\"}", "{\"original-model\": \"replacement-model\"}": "{\"original-model\": \"replacement-model\"}",
"{{category}} Models": "Модели {{category}}", "{{category}} Models": "Модели {{category}}",
"{{completed}}/{{total}} completed": "{{completed}}/{{total}} завершено", "{{completed}}/{{total}} completed": "{{completed}}/{{total}} завершено",
...@@ -876,6 +877,7 @@ ...@@ -876,6 +877,7 @@
"Configure Waffo payment aggregation platform integration": "Настроить интеграцию платёжной платформы Waffo", "Configure Waffo payment aggregation platform integration": "Настроить интеграцию платёжной платформы Waffo",
"Configure your account behavior preferences": "Настроить предпочтения поведения вашей учетной записи", "Configure your account behavior preferences": "Настроить предпочтения поведения вашей учетной записи",
"Configure your account preferences and integrations": "Настроить параметры и интеграции вашей учетной записи", "Configure your account preferences and integrations": "Настроить параметры и интеграции вашей учетной записи",
"Configured as PayMethods JSON. The type value decides which payment flow is used: stripe for Stripe, waffo_pancake for Waffo Pancake, and other values are sent to Epay as the type parameter.": "Сохраняется как JSON PayMethods. Значение type определяет платежный сценарий: stripe для Stripe, waffo_pancake для Waffo Pancake, остальные значения отправляются в Epay как параметр type.",
"Configured routes and latency checks": "Настроенные маршруты и проверки задержки", "Configured routes and latency checks": "Настроенные маршруты и проверки задержки",
"Confirm": "Подтверждение", "Confirm": "Подтверждение",
"Confirm Action": "Подтвердить действие", "Confirm Action": "Подтвердить действие",
...@@ -1081,6 +1083,7 @@ ...@@ -1081,6 +1083,7 @@
"Custom Currency Symbol": "Пользовательский символ валюты", "Custom Currency Symbol": "Пользовательский символ валюты",
"Custom currency symbol is required": "Пользовательский символ валюты обязателен", "Custom currency symbol is required": "Пользовательский символ валюты обязателен",
"Custom database driver detected.": "Обнаружен пользовательский драйвер базы данных.", "Custom database driver detected.": "Обнаружен пользовательский драйвер базы данных.",
"Custom Epay method": "Пользовательский метод Epay",
"Custom Error Response": "Пользовательский ответ с ошибкой", "Custom Error Response": "Пользовательский ответ с ошибкой",
"Custom Home Page": "Пользовательская домашняя страница", "Custom Home Page": "Пользовательская домашняя страница",
"Custom message shown when access is denied": "Пользовательское сообщение, отображаемое при отказе в доступе", "Custom message shown when access is denied": "Пользовательское сообщение, отображаемое при отказе в доступе",
...@@ -1349,6 +1352,7 @@ ...@@ -1349,6 +1352,7 @@
"e.g., preview": "например, preview", "e.g., preview": "например, preview",
"e.g., prod_xxx": "напр., prod_xxx", "e.g., prod_xxx": "напр., prod_xxx",
"e.g., Recommended for China Mainland Users": "например, Рекомендуется для пользователей материкового Китая", "e.g., Recommended for China Mainland Users": "например, Рекомендуется для пользователей материкового Китая",
"e.g., SiAlipay": "например, SiAlipay",
"e.g., us-central1 or JSON format for model-specific regions": "например, us-central1 или формат JSON для регионов, специфичных для модели", "e.g., us-central1 or JSON format for model-specific regions": "например, us-central1 или формат JSON для регионов, специфичных для модели",
"e.g., v2.1": "например, v2.1", "e.g., v2.1": "например, v2.1",
"Each backup code can only be used once.": "Каждый код восстановления можно использовать только один раз.", "Each backup code can only be used once.": "Каждый код восстановления можно использовать только один раз.",
...@@ -1471,6 +1475,7 @@ ...@@ -1471,6 +1475,7 @@
"Enter a name": "Введите имя", "Enter a name": "Введите имя",
"Enter a new name": "Введите новое имя", "Enter a new name": "Введите новое имя",
"Enter a positive or negative amount to adjust the quota": "Введите положительную или отрицательную сумму для корректировки квоты", "Enter a positive or negative amount to adjust the quota": "Введите положительную или отрицательную сумму для корректировки квоты",
"Enter a react-icons component name. Invalid names show no icon.": "Введите имя компонента react-icons. Недопустимые имена не отображают значок.",
"Enter a valid email or leave blank": "Введите действительный email или оставьте пустым", "Enter a valid email or leave blank": "Введите действительный email или оставьте пустым",
"Enter a value and press Enter": "Введите значение и нажмите Enter", "Enter a value and press Enter": "Введите значение и нажмите Enter",
"Enter amount in {{currency}}": "Введите сумму в {{currency}}", "Enter amount in {{currency}}": "Введите сумму в {{currency}}",
...@@ -1505,6 +1510,7 @@ ...@@ -1505,6 +1510,7 @@
"Enter one API key per line for batch creation": "Введите по одному API-ключу на строку для пакетного создания", "Enter one API key per line for batch creation": "Введите по одному API-ключу на строку для пакетного создания",
"Enter one key per line for batch creation": "Введите по одному ключу на строку для пакетного создания", "Enter one key per line for batch creation": "Введите по одному ключу на строку для пакетного создания",
"Enter one keyword per line": "Введите по одному ключевому слову на строку", "Enter one keyword per line": "Введите по одному ключевому слову на строку",
"Enter only a top-level callback domain, for example https://api.example.com, without any path.": "Введите только домен верхнего уровня для callback, например https://api.example.com, без пути.",
"Enter password": "Введите пароль", "Enter password": "Введите пароль",
"Enter password (8-20 characters)": "Введите пароль (8-20 символов)", "Enter password (8-20 characters)": "Введите пароль (8-20 символов)",
"Enter password (min 8 characters)": "Введите пароль (минимум 8 символов)", "Enter password (min 8 characters)": "Введите пароль (минимум 8 символов)",
...@@ -1541,10 +1547,14 @@ ...@@ -1541,10 +1547,14 @@
"Env (JSON object)": "Env (объект JSON)", "Env (JSON object)": "Env (объект JSON)",
"Environment variables": "Переменные окружения", "Environment variables": "Переменные окружения",
"Environment variables (JSON)": "Переменные окружения (JSON)", "Environment variables (JSON)": "Переменные окружения (JSON)",
"Epay Alipay": "Alipay через Epay",
"Epay endpoint": "Конечная точка Epay", "Epay endpoint": "Конечная точка Epay",
"Epay Gateway": "Шлюз Epay", "Epay Gateway": "Шлюз Epay",
"Epay is a payment protocol, not a specific official website. Verify the provider yourself and do not trust random third-party Epay deployments.": "Epay — это платежный протокол, а не конкретный официальный сайт. Самостоятельно проверяйте поставщика и не доверяйте случайным сторонним развертываниям Epay.",
"Epay merchant ID": "ID торговца EasyPay", "Epay merchant ID": "ID торговца EasyPay",
"Epay safety reminder": "Напоминание о безопасности Epay",
"Epay secret key": "Секретный ключ Epay", "Epay secret key": "Секретный ключ Epay",
"Epay WeChat Pay": "WeChat Pay через Epay",
"Equals": "Равно", "Equals": "Равно",
"Error": "Ошибка", "Error": "Ошибка",
"Error Code (optional)": "Код ошибки (необязательно)", "Error Code (optional)": "Код ошибки (необязательно)",
...@@ -2763,6 +2773,7 @@ ...@@ -2763,6 +2773,7 @@
"Only allow specific email domains": "Разрешить только определенные домены электронной почты", "Only allow specific email domains": "Разрешить только определенные домены электронной почты",
"Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "Доступно только для администраторов. При включении вы будете получать сводное уведомление выбранным способом, когда запланированная проверка моделей обнаружит изменения в вышестоящих моделях или сбои проверки.", "Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "Доступно только для администраторов. При включении вы будете получать сводное уведомление выбранным способом, когда запланированная проверка моделей обнаружит изменения в вышестоящих моделях или сбои проверки.",
"Only configured combinations are overridden. All other calls keep the token group base ratio.": "Переопределяются только настроенные комбинации. Остальные вызовы используют базовый коэффициент группы токена.", "Only configured combinations are overridden. All other calls keep the token group base ratio.": "Переопределяются только настроенные комбинации. Остальные вызовы используют базовый коэффициент группы токена.",
"Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.": "Введите только origin сайта, например https://api.example.com. Не добавляйте пути, например /api/user/epay/notify. Оставьте пустым, чтобы использовать адрес сервера.",
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Будут перезаписаны только выбранные поля. Вы можете повторно запустить мастер синхронизации, если появятся новые конфликты.", "Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Будут перезаписаны только выбранные поля. Вы можете повторно запустить мастер синхронизации, если появятся новые конфликты.",
"Only successful requests": "Только успешные запросы", "Only successful requests": "Только успешные запросы",
"Only successful requests count toward this limit.": "Только успешные запросы учитываются в этом лимите.", "Only successful requests count toward this limit.": "Только успешные запросы учитываются в этом лимите.",
...@@ -2921,6 +2932,8 @@ ...@@ -2921,6 +2932,8 @@
"Payment Gateway": "Платежный шлюз", "Payment Gateway": "Платежный шлюз",
"Payment initiated": "Платёж инициирован", "Payment initiated": "Платёж инициирован",
"Payment Method": "Способ оплаты", "Payment Method": "Способ оплаты",
"Payment method identifier": "Идентификатор способа оплаты",
"Payment method identifier is required": "Идентификатор способа оплаты обязателен",
"Payment method name": "Название способа оплаты", "Payment method name": "Название способа оплаты",
"Payment method name is required": "Название способа оплаты обязательно", "Payment method name is required": "Название способа оплаты обязательно",
"Payment method type": "Тип способа оплаты", "Payment method type": "Тип способа оплаты",
...@@ -2931,6 +2944,8 @@ ...@@ -2931,6 +2944,8 @@
"Payment request failed": "Запрос на оплату не удался", "Payment request failed": "Запрос на оплату не удался",
"Payment return URL": "URL возврата после оплаты", "Payment return URL": "URL возврата после оплаты",
"Payment return URL is empty. Create the product without a SuccessURL redirect?": "URL возврата платежа пуст. Создать продукт без перенаправления SuccessURL?", "Payment return URL is empty. Create the product without a SuccessURL redirect?": "URL возврата платежа пуст. Создать продукт без перенаправления SuccessURL?",
"Payment type key": "Ключ типа оплаты",
"Payment type key is required": "Ключ типа оплаты обязателен",
"Payment, redemption codes, subscription plans, and invitation rewards are locked until the root administrator confirms the compliance terms.": "Платежи, коды пополнения, планы подписки и награды за приглашения заблокированы, пока root-администратор не подтвердит условия соответствия.", "Payment, redemption codes, subscription plans, and invitation rewards are locked until the root administrator confirms the compliance terms.": "Платежи, коды пополнения, планы подписки и награды за приглашения заблокированы, пока root-администратор не подтвердит условия соответствия.",
"Peak": "Пик", "Peak": "Пик",
"Peak throughput": "Пиковая пропускная способность", "Peak throughput": "Пиковая пропускная способность",
...@@ -3189,6 +3204,7 @@ ...@@ -3189,6 +3204,7 @@
"Queued": "В очереди", "Queued": "В очереди",
"Quick actions": "Быстрые действия", "Quick actions": "Быстрые действия",
"Quick insert common payment methods": "Быстрая вставка распространенных способов оплаты", "Quick insert common payment methods": "Быстрая вставка распространенных способов оплаты",
"Quick insert payment entries": "Быстро вставить платежные записи",
"Quick Range": "Быстрый диапазон", "Quick Range": "Быстрый диапазон",
"Quick Setup from Preset": "Быстрая настройка из предустановки", "Quick Setup from Preset": "Быстрая настройка из предустановки",
"Quota": "Квота", "Quota": "Квота",
...@@ -3546,6 +3562,7 @@ ...@@ -3546,6 +3562,7 @@
"Search feature in development": "Функция поиска в разработке", "Search feature in development": "Функция поиска в разработке",
"Search group names...": "Поиск названий групп...", "Search group names...": "Поиск названий групп...",
"Search groups...": "Поиск групп...", "Search groups...": "Поиск групп...",
"Search method identifiers...": "Поиск идентификаторов способов...",
"Search missing models": "Поиск отсутствующих моделей", "Search missing models": "Поиск отсутствующих моделей",
"Search model name, provider, endpoint, or tag...": "Поиск по названию модели, поставщику, endpoint или тегу...", "Search model name, provider, endpoint, or tag...": "Поиск по названию модели, поставщику, endpoint или тегу...",
"Search model name...": "Поиск имени модели...", "Search model name...": "Поиск имени модели...",
...@@ -3553,6 +3570,7 @@ ...@@ -3553,6 +3570,7 @@
"Search models or fields...": "Поиск моделей или полей...", "Search models or fields...": "Поиск моделей или полей...",
"Search models...": "Поиск моделей...", "Search models...": "Поиск моделей...",
"Search payment methods...": "Поиск способов оплаты...", "Search payment methods...": "Поиск способов оплаты...",
"Search payment type keys...": "Поиск ключей типа оплаты...",
"Search payment types...": "Поиск типов оплаты...", "Search payment types...": "Поиск типов оплаты...",
"Search products...": "Поиск продуктов...", "Search products...": "Поиск продуктов...",
"Search rules...": "Поиск правил…", "Search rules...": "Поиск правил…",
...@@ -3621,8 +3639,10 @@ ...@@ -3621,8 +3639,10 @@
"Select models to process. Unselected \"add\" models will be ignored.": "Выберите модели для обработки. Невыбранные модели «добавить» будут проигнорированы.", "Select models to process. Unselected \"add\" models will be ignored.": "Выберите модели для обработки. Невыбранные модели «добавить» будут проигнорированы.",
"Select models to run batch tests.": "Выберите модели для запуска пакетных тестов.", "Select models to run batch tests.": "Выберите модели для запуска пакетных тестов.",
"Select or enter color value": "Выбрать или ввести значение цвета", "Select or enter color value": "Выбрать или ввести значение цвета",
"Select or enter method identifier": "Выберите или введите идентификатор способа",
"Select or enter model name": "Выберите или введите имя модели", "Select or enter model name": "Выберите или введите имя модели",
"Select or enter payment type": "Выбрать или ввести тип оплаты", "Select or enter payment type": "Выбрать или ввести тип оплаты",
"Select or enter payment type key": "Выберите или введите ключ типа оплаты",
"Select payment method": "Выберите способ оплаты", "Select payment method": "Выберите способ оплаты",
"Select preset or enter custom CSS color value.": "Выберите предустановку или введите пользовательское значение цвета CSS.", "Select preset or enter custom CSS color value.": "Выберите предустановку или введите пользовательское значение цвета CSS.",
"Select publish date": "Выберите дату публикации", "Select publish date": "Выберите дату публикации",
...@@ -4021,6 +4041,7 @@ ...@@ -4021,6 +4041,7 @@
"This expression is too complex for the visual editor. Please switch to expression mode to edit.": "Для визуального редактора это выражение слишком сложно. Переключитесь в режим выражения для правки.", "This expression is too complex for the visual editor. Please switch to expression mode to edit.": "Для визуального редактора это выражение слишком сложно. Переключитесь в режим выражения для правки.",
"This feature is experimental. Configuration format and behavior may change.": "Эта функция является экспериментальной. Формат конфигурации и поведение могут измениться.", "This feature is experimental. Configuration format and behavior may change.": "Эта функция является экспериментальной. Формат конфигурации и поведение могут измениться.",
"This feature requires server-side WeChat configuration": "Эта функция требует серверной конфигурации WeChat", "This feature requires server-side WeChat configuration": "Эта функция требует серверной конфигурации WeChat",
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "Этот идентификатор отправляется в платежный backend при создании заказа. Для Alipay используйте alipay, для WeChat Pay — wxpay, для Stripe — stripe. Пользовательские значения должны поддерживаться вашим платежным провайдером.",
"This may cause cache failures.": "Это может привести к сбоям кэша.", "This may cause cache failures.": "Это может привести к сбоям кэша.",
"This may take a few moments while we validate the request and update your session.": "Это может занять несколько мгновений, пока мы проверяем запрос и обновляем вашу сессию.", "This may take a few moments while we validate the request and update your session.": "Это может занять несколько мгновений, пока мы проверяем запрос и обновляем вашу сессию.",
"This model has both fixed price and ratio billing conflicts": "Эта модель имеет конфликты как фиксированной цены, так и пропорциональной тарификации", "This model has both fixed price and ratio billing conflicts": "Эта модель имеет конфликты как фиксированной цены, так и пропорциональной тарификации",
...@@ -4357,6 +4378,7 @@ ...@@ -4357,6 +4378,7 @@
"Used Quota": "Лимит потребления", "Used Quota": "Лимит потребления",
"Used to authenticate with io.net deployment API": "Используется для аутентификации в API развертывания io.net", "Used to authenticate with io.net deployment API": "Используется для аутентификации в API развертывания io.net",
"Used to authenticate with the worker. Leave blank to keep the existing secret.": "Используется для аутентификации с воркером. Оставьте пустым, чтобы сохранить существующий секрет.", "Used to authenticate with the worker. Leave blank to keep the existing secret.": "Используется для аутентификации с воркером. Оставьте пустым, чтобы сохранить существующий секрет.",
"Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.": "Определяет платежный сценарий после нажатия. Встроенные ключи: stripe для Stripe и waffo_pancake для Waffo Pancake; остальные значения отправляются в Epay как параметр type.",
"Used:": "Использовано:", "Used:": "Использовано:",
"User": "Пользователь", "User": "Пользователь",
"User {{id}}": "Пользователь {{id}}", "User {{id}}": "Пользователь {{id}}",
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
"_copy": "_bản sao", "_copy": "_bản sao",
",": ", ", ",": ", ",
", and": ", và", ", and": ", và",
",and ": ", and ", ",and ": " ",
"、": ", ", "、": ", ",
"? This action cannot be undone.": "? Hành động này không thể hoàn tác.", "? This action cannot be undone.": "? Hành động này không thể hoàn tác.",
". Please fix the JSON before saving.": ". Vui lòng sửa JSON trước khi lưu.", ". Please fix the JSON before saving.": ". Vui lòng sửa JSON trước khi lưu.",
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"(Override all channels' models)": "(Ghi đè các mô hình của tất cả các kênh)", "(Override all channels' models)": "(Ghi đè các mô hình của tất cả các kênh)",
"[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]": "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]", "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]": "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]",
"[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]", "[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]",
"[{\"name\":\"支付宝\",\"type\":\"alipay\",\"icon\":\"SiAlipay\"}]": "[{\"name\":\"Alipay\",\"type\":\"alipay\",\"icon\":\"SiAlipay\"}]",
"{\"original-model\": \"replacement-model\"}": "{\"original-model\": \"replacement-model\"}", "{\"original-model\": \"replacement-model\"}": "{\"original-model\": \"replacement-model\"}",
"{{category}} Models": "Mô hình {{category}}", "{{category}} Models": "Mô hình {{category}}",
"{{completed}}/{{total}} completed": "Đã hoàn tất {{completed}}/{{total}}", "{{completed}}/{{total}} completed": "Đã hoàn tất {{completed}}/{{total}}",
...@@ -876,6 +877,7 @@ ...@@ -876,6 +877,7 @@
"Configure Waffo payment aggregation platform integration": "Cấu hình tích hợp nền tảng tổng hợp thanh toán Waffo", "Configure Waffo payment aggregation platform integration": "Cấu hình tích hợp nền tảng tổng hợp thanh toán Waffo",
"Configure your account behavior preferences": "Cấu hình tùy chọn hành vi tài khoản của bạn", "Configure your account behavior preferences": "Cấu hình tùy chọn hành vi tài khoản của bạn",
"Configure your account preferences and integrations": "Cấu hình các tùy chọn và tích hợp tài khoản của bạn", "Configure your account preferences and integrations": "Cấu hình các tùy chọn và tích hợp tài khoản của bạn",
"Configured as PayMethods JSON. The type value decides which payment flow is used: stripe for Stripe, waffo_pancake for Waffo Pancake, and other values are sent to Epay as the type parameter.": "Được lưu dưới dạng JSON PayMethods. Giá trị type quyết định luồng thanh toán sẽ dùng: stripe cho Stripe, waffo_pancake cho Waffo Pancake, các giá trị khác được gửi tới Epay dưới dạng tham số type.",
"Configured routes and latency checks": "Tuyến đã cấu hình và kiểm tra độ trễ", "Configured routes and latency checks": "Tuyến đã cấu hình và kiểm tra độ trễ",
"Confirm": "Xác nhận", "Confirm": "Xác nhận",
"Confirm Action": "Xác nhận hành động", "Confirm Action": "Xác nhận hành động",
...@@ -1081,6 +1083,7 @@ ...@@ -1081,6 +1083,7 @@
"Custom Currency Symbol": "Ký hiệu tiền tệ tùy chỉnh", "Custom Currency Symbol": "Ký hiệu tiền tệ tùy chỉnh",
"Custom currency symbol is required": "Ký hiệu tiền tệ tùy chỉnh là bắt buộc", "Custom currency symbol is required": "Ký hiệu tiền tệ tùy chỉnh là bắt buộc",
"Custom database driver detected.": "Đã phát hiện trình điều khiển cơ sở dữ liệu tùy chỉnh.", "Custom database driver detected.": "Đã phát hiện trình điều khiển cơ sở dữ liệu tùy chỉnh.",
"Custom Epay method": "Phương thức Epay tùy chỉnh",
"Custom Error Response": "Phản hồi lỗi tùy chỉnh", "Custom Error Response": "Phản hồi lỗi tùy chỉnh",
"Custom Home Page": "Trang chủ tùy chỉnh", "Custom Home Page": "Trang chủ tùy chỉnh",
"Custom message shown when access is denied": "Thông báo tùy chỉnh hiển thị khi truy cập bị từ chối", "Custom message shown when access is denied": "Thông báo tùy chỉnh hiển thị khi truy cập bị từ chối",
...@@ -1349,6 +1352,7 @@ ...@@ -1349,6 +1352,7 @@
"e.g., preview": "ví dụ: xem trước", "e.g., preview": "ví dụ: xem trước",
"e.g., prod_xxx": "ví dụ, prod_xxx", "e.g., prod_xxx": "ví dụ, prod_xxx",
"e.g., Recommended for China Mainland Users": "ví dụ: Khuyến nghị dành cho người dùng Trung Quốc đại lục", "e.g., Recommended for China Mainland Users": "ví dụ: Khuyến nghị dành cho người dùng Trung Quốc đại lục",
"e.g., SiAlipay": "ví dụ: SiAlipay",
"e.g., us-central1 or JSON format for model-specific regions": "chẳng hạn như us-central1 hoặc định dạng JSON cho các khu vực dành riêng cho mô hình", "e.g., us-central1 or JSON format for model-specific regions": "chẳng hạn như us-central1 hoặc định dạng JSON cho các khu vực dành riêng cho mô hình",
"e.g., v2.1": "e.g., v2.1", "e.g., v2.1": "e.g., v2.1",
"Each backup code can only be used once.": "Mỗi mã dự phòng chỉ có thể được sử dụng một lần.", "Each backup code can only be used once.": "Mỗi mã dự phòng chỉ có thể được sử dụng một lần.",
...@@ -1471,6 +1475,7 @@ ...@@ -1471,6 +1475,7 @@
"Enter a name": "Nhập tên", "Enter a name": "Nhập tên",
"Enter a new name": "Nhập tên mới", "Enter a new name": "Nhập tên mới",
"Enter a positive or negative amount to adjust the quota": "Nhập một giá trị dương hoặc âm để điều chỉnh hạn ngạch", "Enter a positive or negative amount to adjust the quota": "Nhập một giá trị dương hoặc âm để điều chỉnh hạn ngạch",
"Enter a react-icons component name. Invalid names show no icon.": "Nhập tên component react-icons. Tên không hợp lệ sẽ không hiển thị biểu tượng.",
"Enter a valid email or leave blank": "Nhập email hợp lệ hoặc để trống", "Enter a valid email or leave blank": "Nhập email hợp lệ hoặc để trống",
"Enter a value and press Enter": "Nhập giá trị và nhấn Enter", "Enter a value and press Enter": "Nhập giá trị và nhấn Enter",
"Enter amount in {{currency}}": "Nhập số tiền bằng {{currency}}", "Enter amount in {{currency}}": "Nhập số tiền bằng {{currency}}",
...@@ -1505,6 +1510,7 @@ ...@@ -1505,6 +1510,7 @@
"Enter one API key per line for batch creation": "Nhập một khóa API mỗi dòng để tạo hàng loạt", "Enter one API key per line for batch creation": "Nhập một khóa API mỗi dòng để tạo hàng loạt",
"Enter one key per line for batch creation": "Nhập một khóa mỗi dòng để tạo hàng loạt", "Enter one key per line for batch creation": "Nhập một khóa mỗi dòng để tạo hàng loạt",
"Enter one keyword per line": "Nhập một từ khóa mỗi dòng", "Enter one keyword per line": "Nhập một từ khóa mỗi dòng",
"Enter only a top-level callback domain, for example https://api.example.com, without any path.": "Chỉ nhập tên miền callback cấp cao nhất, ví dụ https://api.example.com, không kèm đường dẫn.",
"Enter password": "Nhập mật khẩu", "Enter password": "Nhập mật khẩu",
"Enter password (8-20 characters)": "Nhập mật khẩu (8-20 ký tự)", "Enter password (8-20 characters)": "Nhập mật khẩu (8-20 ký tự)",
"Enter password (min 8 characters)": "Nhập mật khẩu (tối thiểu 8 ký tự)", "Enter password (min 8 characters)": "Nhập mật khẩu (tối thiểu 8 ký tự)",
...@@ -1541,10 +1547,14 @@ ...@@ -1541,10 +1547,14 @@
"Env (JSON object)": "Env (đối tượng JSON)", "Env (JSON object)": "Env (đối tượng JSON)",
"Environment variables": "Biến môi trường", "Environment variables": "Biến môi trường",
"Environment variables (JSON)": "Biến môi trường (JSON)", "Environment variables (JSON)": "Biến môi trường (JSON)",
"Epay Alipay": "Alipay qua Epay",
"Epay endpoint": "Epay điểm cuối", "Epay endpoint": "Epay điểm cuối",
"Epay Gateway": "Cổng thanh toán Epay", "Epay Gateway": "Cổng thanh toán Epay",
"Epay is a payment protocol, not a specific official website. Verify the provider yourself and do not trust random third-party Epay deployments.": "Epay là một giao thức thanh toán, không phải một trang web chính thức cụ thể. Hãy tự xác minh nhà cung cấp và không tin tùy tiện các bản triển khai Epay của bên thứ ba.",
"Epay merchant ID": "ID người bán Epay", "Epay merchant ID": "ID người bán Epay",
"Epay safety reminder": "Nhắc nhở an toàn Epay",
"Epay secret key": "Khóa bí mật Epay", "Epay secret key": "Khóa bí mật Epay",
"Epay WeChat Pay": "WeChat Pay qua Epay",
"Equals": "Bằng", "Equals": "Bằng",
"Error": "Lỗi", "Error": "Lỗi",
"Error Code (optional)": "Mã lỗi (tùy chọn)", "Error Code (optional)": "Mã lỗi (tùy chọn)",
...@@ -2763,6 +2773,7 @@ ...@@ -2763,6 +2773,7 @@
"Only allow specific email domains": "Chỉ cho phép các tên miền email cụ thể", "Only allow specific email domains": "Chỉ cho phép các tên miền email cụ thể",
"Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "Chỉ khả dụng cho quản trị viên. Khi bật, bạn sẽ nhận được thông báo tổng hợp qua phương thức đã chọn khi kiểm tra mô hình định kỳ phát hiện thay đổi mô hình nguồn hoặc lỗi kiểm tra.", "Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "Chỉ khả dụng cho quản trị viên. Khi bật, bạn sẽ nhận được thông báo tổng hợp qua phương thức đã chọn khi kiểm tra mô hình định kỳ phát hiện thay đổi mô hình nguồn hoặc lỗi kiểm tra.",
"Only configured combinations are overridden. All other calls keep the token group base ratio.": "Chỉ các tổ hợp đã cấu hình mới bị ghi đè. Các lệnh gọi khác giữ tỷ lệ cơ bản của nhóm token.", "Only configured combinations are overridden. All other calls keep the token group base ratio.": "Chỉ các tổ hợp đã cấu hình mới bị ghi đè. Các lệnh gọi khác giữ tỷ lệ cơ bản của nhóm token.",
"Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.": "Chỉ nhập origin của trang, ví dụ https://api.example.com. Không nhập đường dẫn như /api/user/epay/notify. Để trống để dùng địa chỉ máy chủ.",
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Chỉ các trường được chọn sẽ bị ghi đè. Bạn có thể chạy lại trình hướng dẫn đồng bộ hóa nếu có xung đột mới xuất hiện.", "Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Chỉ các trường được chọn sẽ bị ghi đè. Bạn có thể chạy lại trình hướng dẫn đồng bộ hóa nếu có xung đột mới xuất hiện.",
"Only successful requests": "Chỉ các yêu cầu thành công", "Only successful requests": "Chỉ các yêu cầu thành công",
"Only successful requests count toward this limit.": "Chỉ những yêu cầu thành công mới được tính vào giới hạn này.", "Only successful requests count toward this limit.": "Chỉ những yêu cầu thành công mới được tính vào giới hạn này.",
...@@ -2792,7 +2803,7 @@ ...@@ -2792,7 +2803,7 @@
"OpenRouter": "OpenRouter", "OpenRouter": "OpenRouter",
"opens in an external client. Trigger it from the sidebar or API key actions to launch the configured application.": "mở trong một ứng dụng bên ngoài. Kích hoạt nó từ thanh bên hoặc các hành động khóa API để khởi chạy ứng dụng đã cấu hình.", "opens in an external client. Trigger it from the sidebar or API key actions to launch the configured application.": "mở trong một ứng dụng bên ngoài. Kích hoạt nó từ thanh bên hoặc các hành động khóa API để khởi chạy ứng dụng đã cấu hình.",
"Operation": "Thao tác", "Operation": "Thao tác",
"operation and charging behavior": "hành vi vận hành và thu phí", "operation and charging behavior": "vận hành và thu phí",
"Operation Audit Info": "Thông tin kiểm toán thao tác", "Operation Audit Info": "Thông tin kiểm toán thao tác",
"Operation failed": "Thao tác thất bại", "Operation failed": "Thao tác thất bại",
"Operation successful": "Thao tác thành công", "Operation successful": "Thao tác thành công",
...@@ -2921,6 +2932,8 @@ ...@@ -2921,6 +2932,8 @@
"Payment Gateway": "Cổng thanh toán", "Payment Gateway": "Cổng thanh toán",
"Payment initiated": "Đã khởi tạo thanh toán", "Payment initiated": "Đã khởi tạo thanh toán",
"Payment Method": "Phương thức thanh toán", "Payment Method": "Phương thức thanh toán",
"Payment method identifier": "Mã định danh phương thức thanh toán",
"Payment method identifier is required": "Mã định danh phương thức thanh toán là bắt buộc",
"Payment method name": "Tên phương thức thanh toán", "Payment method name": "Tên phương thức thanh toán",
"Payment method name is required": "Tên phương thức thanh toán là bắt buộc", "Payment method name is required": "Tên phương thức thanh toán là bắt buộc",
"Payment method type": "Loại phương thức thanh toán", "Payment method type": "Loại phương thức thanh toán",
...@@ -2931,6 +2944,8 @@ ...@@ -2931,6 +2944,8 @@
"Payment request failed": "Yêu cầu thanh toán thất bại", "Payment request failed": "Yêu cầu thanh toán thất bại",
"Payment return URL": "URL trả về thanh toán", "Payment return URL": "URL trả về thanh toán",
"Payment return URL is empty. Create the product without a SuccessURL redirect?": "URL trả về thanh toán đang trống. Tạo sản phẩm mà không có chuyển hướng SuccessURL?", "Payment return URL is empty. Create the product without a SuccessURL redirect?": "URL trả về thanh toán đang trống. Tạo sản phẩm mà không có chuyển hướng SuccessURL?",
"Payment type key": "Khóa loại thanh toán",
"Payment type key is required": "Khóa loại thanh toán là bắt buộc",
"Payment, redemption codes, subscription plans, and invitation rewards are locked until the root administrator confirms the compliance terms.": "Thanh toán, mã đổi thưởng, gói đăng ký và phần thưởng mời sẽ bị khóa cho đến khi quản trị viên root xác nhận điều khoản tuân thủ.", "Payment, redemption codes, subscription plans, and invitation rewards are locked until the root administrator confirms the compliance terms.": "Thanh toán, mã đổi thưởng, gói đăng ký và phần thưởng mời sẽ bị khóa cho đến khi quản trị viên root xác nhận điều khoản tuân thủ.",
"Peak": "Đỉnh", "Peak": "Đỉnh",
"Peak throughput": "Thông lượng đỉnh", "Peak throughput": "Thông lượng đỉnh",
...@@ -3189,6 +3204,7 @@ ...@@ -3189,6 +3204,7 @@
"Queued": "Trong hàng đợi", "Queued": "Trong hàng đợi",
"Quick actions": "Thao tác nhanh", "Quick actions": "Thao tác nhanh",
"Quick insert common payment methods": "Chèn nhanh các phương thức thanh toán phổ biến", "Quick insert common payment methods": "Chèn nhanh các phương thức thanh toán phổ biến",
"Quick insert payment entries": "Chèn nhanh mục thanh toán",
"Quick Range": "Phạm vi nhanh", "Quick Range": "Phạm vi nhanh",
"Quick Setup from Preset": "Thiết lập nhanh từ cấu hình sẵn", "Quick Setup from Preset": "Thiết lập nhanh từ cấu hình sẵn",
"Quota": "Hạn ngạch", "Quota": "Hạn ngạch",
...@@ -3546,6 +3562,7 @@ ...@@ -3546,6 +3562,7 @@
"Search feature in development": "Tính năng tìm kiếm đang được phát triển", "Search feature in development": "Tính năng tìm kiếm đang được phát triển",
"Search group names...": "Tìm kiếm tên nhóm...", "Search group names...": "Tìm kiếm tên nhóm...",
"Search groups...": "Searching for group...", "Search groups...": "Searching for group...",
"Search method identifiers...": "Tìm mã định danh phương thức...",
"Search missing models": "Tìm kiếm mô hình bị thiếu", "Search missing models": "Tìm kiếm mô hình bị thiếu",
"Search model name, provider, endpoint, or tag...": "Tìm tên mô hình, nhà cung cấp, endpoint hoặc thẻ...", "Search model name, provider, endpoint, or tag...": "Tìm tên mô hình, nhà cung cấp, endpoint hoặc thẻ...",
"Search model name...": "Tìm kiếm tên mẫu...", "Search model name...": "Tìm kiếm tên mẫu...",
...@@ -3553,6 +3570,7 @@ ...@@ -3553,6 +3570,7 @@
"Search models or fields...": "Tìm kiếm mô hình hoặc trường...", "Search models or fields...": "Tìm kiếm mô hình hoặc trường...",
"Search models...": "Mô hình tìm kiếm...", "Search models...": "Mô hình tìm kiếm...",
"Search payment methods...": "Tìm kiếm phương thức thanh toán...", "Search payment methods...": "Tìm kiếm phương thức thanh toán...",
"Search payment type keys...": "Tìm khóa loại thanh toán...",
"Search payment types...": "Tìm kiếm loại thanh toán...", "Search payment types...": "Tìm kiếm loại thanh toán...",
"Search products...": "Tìm kiếm sản phẩm...", "Search products...": "Tìm kiếm sản phẩm...",
"Search rules...": "Tìm kiếm quy tắc…", "Search rules...": "Tìm kiếm quy tắc…",
...@@ -3621,8 +3639,10 @@ ...@@ -3621,8 +3639,10 @@
"Select models to process. Unselected \"add\" models will be ignored.": "Chọn các mô hình để xử lý. Các mô hình \"thêm\" không được chọn sẽ bị bỏ qua.", "Select models to process. Unselected \"add\" models will be ignored.": "Chọn các mô hình để xử lý. Các mô hình \"thêm\" không được chọn sẽ bị bỏ qua.",
"Select models to run batch tests.": "Chọn mô hình để chạy kiểm thử hàng loạt.", "Select models to run batch tests.": "Chọn mô hình để chạy kiểm thử hàng loạt.",
"Select or enter color value": "Chọn hoặc nhập giá trị màu", "Select or enter color value": "Chọn hoặc nhập giá trị màu",
"Select or enter method identifier": "Chọn hoặc nhập mã định danh phương thức",
"Select or enter model name": "Chọn hoặc nhập tên mô hình", "Select or enter model name": "Chọn hoặc nhập tên mô hình",
"Select or enter payment type": "Chọn hoặc nhập loại thanh toán", "Select or enter payment type": "Chọn hoặc nhập loại thanh toán",
"Select or enter payment type key": "Chọn hoặc nhập khóa loại thanh toán",
"Select payment method": "Chọn phương thức thanh toán", "Select payment method": "Chọn phương thức thanh toán",
"Select preset or enter custom CSS color value.": "Chọn cài đặt sẵn hoặc nhập giá trị màu CSS tùy chỉnh.", "Select preset or enter custom CSS color value.": "Chọn cài đặt sẵn hoặc nhập giá trị màu CSS tùy chỉnh.",
"Select publish date": "Chọn ngày xuất bản", "Select publish date": "Chọn ngày xuất bản",
...@@ -4021,6 +4041,7 @@ ...@@ -4021,6 +4041,7 @@
"This expression is too complex for the visual editor. Please switch to expression mode to edit.": "Biểu thức này quá phức tạp cho trình sửa trực quan. Hãy chuyển sang chế độ biểu thức để chỉnh sửa.", "This expression is too complex for the visual editor. Please switch to expression mode to edit.": "Biểu thức này quá phức tạp cho trình sửa trực quan. Hãy chuyển sang chế độ biểu thức để chỉnh sửa.",
"This feature is experimental. Configuration format and behavior may change.": "Tính năng này đang ở giai đoạn thử nghiệm. Định dạng cấu hình và hành vi có thể thay đổi.", "This feature is experimental. Configuration format and behavior may change.": "Tính năng này đang ở giai đoạn thử nghiệm. Định dạng cấu hình và hành vi có thể thay đổi.",
"This feature requires server-side WeChat configuration": "Tính năng này yêu cầu cấu hình WeChat phía máy chủ", "This feature requires server-side WeChat configuration": "Tính năng này yêu cầu cấu hình WeChat phía máy chủ",
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "Mã định danh này được gửi tới backend thanh toán khi tạo đơn hàng. Dùng alipay cho Alipay, wxpay cho WeChat Pay, stripe cho Stripe. Giá trị tùy chỉnh phải được nhà cung cấp thanh toán hỗ trợ.",
"This may cause cache failures.": "Điều này có thể gây ra lỗi bộ nhớ đệm.", "This may cause cache failures.": "Điều này có thể gây ra lỗi bộ nhớ đệm.",
"This may take a few moments while we validate the request and update your session.": "Việc này có thể mất vài phút trong khi chúng tôi xác thực yêu cầu và cập nhật phiên của bạn.", "This may take a few moments while we validate the request and update your session.": "Việc này có thể mất vài phút trong khi chúng tôi xác thực yêu cầu và cập nhật phiên của bạn.",
"This model has both fixed price and ratio billing conflicts": "Mô hình này có cả mâu thuẫn về thanh toán theo giá cố định và theo tỷ lệ.", "This model has both fixed price and ratio billing conflicts": "Mô hình này có cả mâu thuẫn về thanh toán theo giá cố định và theo tỷ lệ.",
...@@ -4357,6 +4378,7 @@ ...@@ -4357,6 +4378,7 @@
"Used Quota": "Hạn mức đã sử dụng", "Used Quota": "Hạn mức đã sử dụng",
"Used to authenticate with io.net deployment API": "Dùng để xác thực với API triển khai io.net", "Used to authenticate with io.net deployment API": "Dùng để xác thực với API triển khai io.net",
"Used to authenticate with the worker. Leave blank to keep the existing secret.": "Dùng để xác thực với worker. Để trống để giữ nguyên secret hiện có.", "Used to authenticate with the worker. Leave blank to keep the existing secret.": "Dùng để xác thực với worker. Để trống để giữ nguyên secret hiện có.",
"Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.": "Dùng để quyết định luồng thanh toán khi người dùng nhấp. Các khóa có sẵn gồm stripe cho Stripe và waffo_pancake cho Waffo Pancake; các giá trị khác được gửi tới Epay dưới dạng tham số type.",
"Used:": "Đã dùng:", "Used:": "Đã dùng:",
"User": "Người dùng", "User": "Người dùng",
"User {{id}}": "Người dùng {{id}}", "User {{id}}": "Người dùng {{id}}",
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
"_copy": "_复制", "_copy": "_复制",
",": ",", ",": ",",
", and": ",和", ", and": ",和",
",and ": ",", ",and ": ",",
"、": "、", "、": "、",
"? This action cannot be undone.": "?此操作无法撤销。", "? This action cannot be undone.": "?此操作无法撤销。",
". Please fix the JSON before saving.": "。请在保存前修复 JSON。", ". Please fix the JSON before saving.": "。请在保存前修复 JSON。",
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"(Override all channels' models)": "覆盖所有渠道的模型", "(Override all channels' models)": "覆盖所有渠道的模型",
"[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]": "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]", "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]": "[{\"ChatGPT\":\"https://chat.openai.com\"},{\"Lobe Chat\":\"https://chat-preview.lobehub.com/?settings={...}\"}]",
"[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]": "[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]", "[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]": "[{\"name\":\"支付宝\",\"type\":\"alipay\",\"color\":\"#1677FF\"}]",
"[{\"name\":\"支付宝\",\"type\":\"alipay\",\"icon\":\"SiAlipay\"}]": "[{\"name\":\"支付宝\",\"type\":\"alipay\",\"icon\":\"SiAlipay\"}]",
"{\"original-model\": \"replacement-model\"}": "{\"original-model\": \"replacement-model\"}", "{\"original-model\": \"replacement-model\"}": "{\"original-model\": \"replacement-model\"}",
"{{category}} Models": "{{category}} 模型", "{{category}} Models": "{{category}} 模型",
"{{completed}}/{{total}} completed": "已完成 {{completed}}/{{total}}", "{{completed}}/{{total}} completed": "已完成 {{completed}}/{{total}}",
...@@ -876,6 +877,7 @@ ...@@ -876,6 +877,7 @@
"Configure Waffo payment aggregation platform integration": "配置 Waffo 支付聚合平台集成", "Configure Waffo payment aggregation platform integration": "配置 Waffo 支付聚合平台集成",
"Configure your account behavior preferences": "配置您的账户行为偏好", "Configure your account behavior preferences": "配置您的账户行为偏好",
"Configure your account preferences and integrations": "配置您的账户偏好和集成", "Configure your account preferences and integrations": "配置您的账户偏好和集成",
"Configured as PayMethods JSON. The type value decides which payment flow is used: stripe for Stripe, waffo_pancake for Waffo Pancake, and other values are sent to Epay as the type parameter.": "保存为 PayMethods JSON。type 值决定点击后使用哪个支付流程:stripe 走 Stripe,waffo_pancake 走 Waffo Pancake,其他值作为 Epay 的 type 参数提交。",
"Configured routes and latency checks": "已配置路由和延迟检测", "Configured routes and latency checks": "已配置路由和延迟检测",
"Confirm": "确认", "Confirm": "确认",
"Confirm Action": "确认操作", "Confirm Action": "确认操作",
...@@ -901,7 +903,7 @@ ...@@ -901,7 +903,7 @@
"Confirm Payment": "确认付款", "Confirm Payment": "确认付款",
"Confirm Selection": "确认选择", "Confirm Selection": "确认选择",
"Confirm settings and finish setup": "确认设置并完成安装", "Confirm settings and finish setup": "确认设置并完成安装",
"confirm that I bear legal responsibility arising from deployment": "确认我承担因部署产生的法律责任", "confirm that I bear legal responsibility arising from deployment": "确认承担因部署",
"Confirm Unbind": "确认解绑", "Confirm Unbind": "确认解绑",
"Confirm your identity before removing this Passkey from your account.": "在解绑当前账号的 Passkey 前请确认你的身份。", "Confirm your identity before removing this Passkey from your account.": "在解绑当前账号的 Passkey 前请确认你的身份。",
"Confirm your identity with Two-factor Authentication before registering a Passkey.": "在注册 Passkey 前请使用两步验证确认你的身份。", "Confirm your identity with Two-factor Authentication before registering a Passkey.": "在注册 Passkey 前请使用两步验证确认你的身份。",
...@@ -1081,6 +1083,7 @@ ...@@ -1081,6 +1083,7 @@
"Custom Currency Symbol": "自定义货币符号", "Custom Currency Symbol": "自定义货币符号",
"Custom currency symbol is required": "自定义货币符号为必填项", "Custom currency symbol is required": "自定义货币符号为必填项",
"Custom database driver detected.": "检测到自定义数据库驱动。", "Custom database driver detected.": "检测到自定义数据库驱动。",
"Custom Epay method": "自定义 Epay 方式",
"Custom Error Response": "自定义错误响应", "Custom Error Response": "自定义错误响应",
"Custom Home Page": "自定义主页", "Custom Home Page": "自定义主页",
"Custom message shown when access is denied": "访问被拒绝时显示的自定义消息", "Custom message shown when access is denied": "访问被拒绝时显示的自定义消息",
...@@ -1349,6 +1352,7 @@ ...@@ -1349,6 +1352,7 @@
"e.g., preview": "例如,preview", "e.g., preview": "例如,preview",
"e.g., prod_xxx": "例如,prod_xxx", "e.g., prod_xxx": "例如,prod_xxx",
"e.g., Recommended for China Mainland Users": "例如,推荐给中国大陆用户", "e.g., Recommended for China Mainland Users": "例如,推荐给中国大陆用户",
"e.g., SiAlipay": "例如,SiAlipay",
"e.g., us-central1 or JSON format for model-specific regions": "例如,us-central1 或模型特定区域的 JSON 格式", "e.g., us-central1 or JSON format for model-specific regions": "例如,us-central1 或模型特定区域的 JSON 格式",
"e.g., v2.1": "例如,v2.1", "e.g., v2.1": "例如,v2.1",
"Each backup code can only be used once.": "每个备份代码只能使用一次。", "Each backup code can only be used once.": "每个备份代码只能使用一次。",
...@@ -1471,6 +1475,7 @@ ...@@ -1471,6 +1475,7 @@
"Enter a name": "输入名称", "Enter a name": "输入名称",
"Enter a new name": "输入新名称", "Enter a new name": "输入新名称",
"Enter a positive or negative amount to adjust the quota": "输入正数或负数以调整配额", "Enter a positive or negative amount to adjust the quota": "输入正数或负数以调整配额",
"Enter a react-icons component name. Invalid names show no icon.": "输入 react-icons 组件名。无法解析的名称不会显示图标。",
"Enter a valid email or leave blank": "请输入有效的邮箱地址或留空", "Enter a valid email or leave blank": "请输入有效的邮箱地址或留空",
"Enter a value and press Enter": "输入值并按回车键", "Enter a value and press Enter": "输入值并按回车键",
"Enter amount in {{currency}}": "输入金额({{currency}})", "Enter amount in {{currency}}": "输入金额({{currency}})",
...@@ -1505,6 +1510,7 @@ ...@@ -1505,6 +1510,7 @@
"Enter one API key per line for batch creation": "每行输入一个 API 密钥进行批量创建", "Enter one API key per line for batch creation": "每行输入一个 API 密钥进行批量创建",
"Enter one key per line for batch creation": "每行输入一个密钥进行批量创建", "Enter one key per line for batch creation": "每行输入一个密钥进行批量创建",
"Enter one keyword per line": "每行输入一个关键词", "Enter one keyword per line": "每行输入一个关键词",
"Enter only a top-level callback domain, for example https://api.example.com, without any path.": "只填写回调顶级域名,例如 https://api.example.com,不要带任何路径。",
"Enter password": "输入密码", "Enter password": "输入密码",
"Enter password (8-20 characters)": "输入密码(8-20 个字符)", "Enter password (8-20 characters)": "输入密码(8-20 个字符)",
"Enter password (min 8 characters)": "请输入密码(至少 8 个字符)", "Enter password (min 8 characters)": "请输入密码(至少 8 个字符)",
...@@ -1541,10 +1547,14 @@ ...@@ -1541,10 +1547,14 @@
"Env (JSON object)": "环境变量 (JSON 对象)", "Env (JSON object)": "环境变量 (JSON 对象)",
"Environment variables": "环境变量", "Environment variables": "环境变量",
"Environment variables (JSON)": "环境变量 (JSON)", "Environment variables (JSON)": "环境变量 (JSON)",
"Epay Alipay": "Epay 支付宝",
"Epay endpoint": "Epay 端点", "Epay endpoint": "Epay 端点",
"Epay Gateway": "Epay 网关", "Epay Gateway": "Epay 网关",
"Epay is a payment protocol, not a specific official website. Verify the provider yourself and do not trust random third-party Epay deployments.": "Epay 是一种支付协议,不是某个固定官网。请自行核验服务提供方,不要轻信他人搭建的第三方 Epay 站点。",
"Epay merchant ID": "易支付商户 ID", "Epay merchant ID": "易支付商户 ID",
"Epay safety reminder": "Epay 安全提醒",
"Epay secret key": "Epay 密钥", "Epay secret key": "Epay 密钥",
"Epay WeChat Pay": "Epay 微信支付",
"Equals": "等于", "Equals": "等于",
"Error": "错误", "Error": "错误",
"Error Code (optional)": "错误代码(可选)", "Error Code (optional)": "错误代码(可选)",
...@@ -2763,6 +2773,7 @@ ...@@ -2763,6 +2773,7 @@
"Only allow specific email domains": "仅允许特定的电子邮件域名", "Only allow specific email domains": "仅允许特定的电子邮件域名",
"Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "仅管理员可用。启用后,当定时模型检查检测到上游模型变更或检查失败时,您将通过所选方式收到汇总通知。", "Only available for admins. When enabled, you will receive a summary notification via your selected method when the scheduled model check detects upstream model changes or check failures.": "仅管理员可用。启用后,当定时模型检查检测到上游模型变更或检查失败时,您将通过所选方式收到汇总通知。",
"Only configured combinations are overridden. All other calls keep the token group base ratio.": "只有已配置的组合会被覆盖,其他调用仍使用令牌分组的基础倍率。", "Only configured combinations are overridden. All other calls keep the token group base ratio.": "只有已配置的组合会被覆盖,其他调用仍使用令牌分组的基础倍率。",
"Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.": "只填写站点根域名,例如 https://api.example.com。不要填写 /api/user/epay/notify 这类路径。留空则使用服务器地址。",
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "仅选定的字段将被覆盖。如果出现新的冲突,您可以重新运行同步向导。", "Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "仅选定的字段将被覆盖。如果出现新的冲突,您可以重新运行同步向导。",
"Only successful requests": "仅成功的请求", "Only successful requests": "仅成功的请求",
"Only successful requests count toward this limit.": "仅成功的请求计入此限制。", "Only successful requests count toward this limit.": "仅成功的请求计入此限制。",
...@@ -2792,7 +2803,7 @@ ...@@ -2792,7 +2803,7 @@
"OpenRouter": "OpenRouter", "OpenRouter": "OpenRouter",
"opens in an external client. Trigger it from the sidebar or API key actions to launch the configured application.": "在外部客户端中打开。从侧边栏或 API 密钥操作中触发,以启动配置的应用。", "opens in an external client. Trigger it from the sidebar or API key actions to launch the configured application.": "在外部客户端中打开。从侧边栏或 API 密钥操作中触发,以启动配置的应用。",
"Operation": "操作", "Operation": "操作",
"operation and charging behavior": "运营和收费行为", "operation and charging behavior": "运营和收费行为产生的法律责任",
"Operation Audit Info": "操作审计信息", "Operation Audit Info": "操作审计信息",
"Operation failed": "操作失败", "Operation failed": "操作失败",
"Operation successful": "操作成功", "Operation successful": "操作成功",
...@@ -2921,6 +2932,8 @@ ...@@ -2921,6 +2932,8 @@
"Payment Gateway": "支付网关", "Payment Gateway": "支付网关",
"Payment initiated": "已发起支付", "Payment initiated": "已发起支付",
"Payment Method": "付款方式", "Payment Method": "付款方式",
"Payment method identifier": "支付方式标识",
"Payment method identifier is required": "支付方式标识不能为空",
"Payment method name": "支付方式名称", "Payment method name": "支付方式名称",
"Payment method name is required": "支付方式名称不能为空", "Payment method name is required": "支付方式名称不能为空",
"Payment method type": "支付方式类型", "Payment method type": "支付方式类型",
...@@ -2931,6 +2944,8 @@ ...@@ -2931,6 +2944,8 @@
"Payment request failed": "支付请求失败", "Payment request failed": "支付请求失败",
"Payment return URL": "支付返回地址", "Payment return URL": "支付返回地址",
"Payment return URL is empty. Create the product without a SuccessURL redirect?": "支付返回 URL 为空。是否在没有 SuccessURL 跳转的情况下创建产品?", "Payment return URL is empty. Create the product without a SuccessURL redirect?": "支付返回 URL 为空。是否在没有 SuccessURL 跳转的情况下创建产品?",
"Payment type key": "支付处理标识",
"Payment type key is required": "支付处理标识不能为空",
"Payment, redemption codes, subscription plans, and invitation rewards are locked until the root administrator confirms the compliance terms.": "在根管理员确认合规条款之前,支付、兑换码、订阅套餐和邀请奖励功能将保持锁定。", "Payment, redemption codes, subscription plans, and invitation rewards are locked until the root administrator confirms the compliance terms.": "在根管理员确认合规条款之前,支付、兑换码、订阅套餐和邀请奖励功能将保持锁定。",
"Peak": "峰值", "Peak": "峰值",
"Peak throughput": "峰值吞吐", "Peak throughput": "峰值吞吐",
...@@ -3189,6 +3204,7 @@ ...@@ -3189,6 +3204,7 @@
"Queued": "排队中", "Queued": "排队中",
"Quick actions": "快捷操作", "Quick actions": "快捷操作",
"Quick insert common payment methods": "快速插入常用支付方式", "Quick insert common payment methods": "快速插入常用支付方式",
"Quick insert payment entries": "快速插入支付项",
"Quick Range": "快速范围", "Quick Range": "快速范围",
"Quick Setup from Preset": "从预设快速设置", "Quick Setup from Preset": "从预设快速设置",
"Quota": "额度", "Quota": "额度",
...@@ -3546,6 +3562,7 @@ ...@@ -3546,6 +3562,7 @@
"Search feature in development": "搜索功能开发中", "Search feature in development": "搜索功能开发中",
"Search group names...": "搜索分组名称...", "Search group names...": "搜索分组名称...",
"Search groups...": "搜索分组...", "Search groups...": "搜索分组...",
"Search method identifiers...": "搜索支付方式标识...",
"Search missing models": "搜索缺失的模型", "Search missing models": "搜索缺失的模型",
"Search model name, provider, endpoint, or tag...": "搜索模型名称、供应商、端点或标签...", "Search model name, provider, endpoint, or tag...": "搜索模型名称、供应商、端点或标签...",
"Search model name...": "搜索模型名称...", "Search model name...": "搜索模型名称...",
...@@ -3553,6 +3570,7 @@ ...@@ -3553,6 +3570,7 @@
"Search models or fields...": "搜索模型或字段...", "Search models or fields...": "搜索模型或字段...",
"Search models...": "搜索模型...", "Search models...": "搜索模型...",
"Search payment methods...": "搜索付款方式...", "Search payment methods...": "搜索付款方式...",
"Search payment type keys...": "搜索支付处理标识...",
"Search payment types...": "搜索支付类型...", "Search payment types...": "搜索支付类型...",
"Search products...": "搜索产品...", "Search products...": "搜索产品...",
"Search rules...": "搜索规则…", "Search rules...": "搜索规则…",
...@@ -3621,8 +3639,10 @@ ...@@ -3621,8 +3639,10 @@
"Select models to process. Unselected \"add\" models will be ignored.": "勾选要处理的模型,未勾选的「新增」模型将作为忽略处理。", "Select models to process. Unselected \"add\" models will be ignored.": "勾选要处理的模型,未勾选的「新增」模型将作为忽略处理。",
"Select models to run batch tests.": "选择要运行批量测试的模型。", "Select models to run batch tests.": "选择要运行批量测试的模型。",
"Select or enter color value": "选择或输入颜色值", "Select or enter color value": "选择或输入颜色值",
"Select or enter method identifier": "选择或输入支付方式标识",
"Select or enter model name": "选择或输入模型名称", "Select or enter model name": "选择或输入模型名称",
"Select or enter payment type": "选择或输入支付类型", "Select or enter payment type": "选择或输入支付类型",
"Select or enter payment type key": "选择或输入支付处理标识",
"Select payment method": "选择支付方式", "Select payment method": "选择支付方式",
"Select preset or enter custom CSS color value.": "选择预设或输入自定义 CSS 颜色值。", "Select preset or enter custom CSS color value.": "选择预设或输入自定义 CSS 颜色值。",
"Select publish date": "选择发布日期", "Select publish date": "选择发布日期",
...@@ -4021,6 +4041,7 @@ ...@@ -4021,6 +4041,7 @@
"This expression is too complex for the visual editor. Please switch to expression mode to edit.": "此表达式对可视化编辑器过于复杂,请切换到表达式模式进行编辑。", "This expression is too complex for the visual editor. Please switch to expression mode to edit.": "此表达式对可视化编辑器过于复杂,请切换到表达式模式进行编辑。",
"This feature is experimental. Configuration format and behavior may change.": "此功能为实验性功能。配置格式和行为可能会发生变化。", "This feature is experimental. Configuration format and behavior may change.": "此功能为实验性功能。配置格式和行为可能会发生变化。",
"This feature requires server-side WeChat configuration": "此功能需要服务器端微信配置", "This feature requires server-side WeChat configuration": "此功能需要服务器端微信配置",
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "创建订单时会把这个标识提交给支付后端。支付宝填 alipay,微信填 wxpay,Stripe 填 stripe。自定义值必须是支付服务支持的标识。",
"This may cause cache failures.": "这可能导致缓存故障。", "This may cause cache failures.": "这可能导致缓存故障。",
"This may take a few moments while we validate the request and update your session.": "这可能需要一些时间,因为我们正在验证请求并更新您的会话。", "This may take a few moments while we validate the request and update your session.": "这可能需要一些时间,因为我们正在验证请求并更新您的会话。",
"This model has both fixed price and ratio billing conflicts": "此模型同时存在固定价格和比例计费冲突", "This model has both fixed price and ratio billing conflicts": "此模型同时存在固定价格和比例计费冲突",
...@@ -4357,6 +4378,7 @@ ...@@ -4357,6 +4378,7 @@
"Used Quota": "消耗额度", "Used Quota": "消耗额度",
"Used to authenticate with io.net deployment API": "用于 io.net 部署 API 鉴权", "Used to authenticate with io.net deployment API": "用于 io.net 部署 API 鉴权",
"Used to authenticate with the worker. Leave blank to keep the existing secret.": "用于与 Worker 进行身份验证。留空以保留现有密钥。", "Used to authenticate with the worker. Leave blank to keep the existing secret.": "用于与 Worker 进行身份验证。留空以保留现有密钥。",
"Used to decide the payment flow. Built-in keys include stripe for Stripe and waffo_pancake for Waffo Pancake; other values are sent to Epay as the type parameter.": "用于决定点击后走哪个支付流程。stripe 走 Stripe,waffo_pancake 走 Waffo Pancake;其他值会作为 Epay 的 type 参数提交。",
"Used:": "已使用:", "Used:": "已使用:",
"User": "用户", "User": "用户",
"User {{id}}": "用户 {{id}}", "User {{id}}": "用户 {{id}}",
......
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