mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
chore(ui): redesign Edit Routing Rules modal
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
This commit is contained in:
parent
2a03844566
commit
ba2baa9028
15 changed files with 282 additions and 174 deletions
26
frontend/src/components/ui/TooltipsHelper.tsx
Normal file
26
frontend/src/components/ui/TooltipsHelper.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import {useTranslation} from 'react-i18next';
|
||||||
|
import {Tooltip} from 'antd';
|
||||||
|
import {QuestionCircleOutlined} from '@ant-design/icons';
|
||||||
|
|
||||||
|
export function LabelWithTooltip({labelKey, tooltipKey}: {
|
||||||
|
labelKey: string;
|
||||||
|
tooltipKey: string;
|
||||||
|
}) {
|
||||||
|
const {t} = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={t(tooltipKey)}>
|
||||||
|
{t(labelKey)} <QuestionCircleOutlined/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LabelWithOnePerLineTooltip({labelKey}: {
|
||||||
|
labelKey: string;
|
||||||
|
}) {
|
||||||
|
|
||||||
|
return <LabelWithTooltip
|
||||||
|
labelKey={labelKey}
|
||||||
|
tooltipKey="pages.xray.rules.onePerLine"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { useEffect, useState } from 'react';
|
import {type ChangeEvent, useEffect, useState} from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import {useTranslation} from 'react-i18next';
|
||||||
import { Button, Form, Input, Modal, Select, Space, Tooltip } from 'antd';
|
import {Button, Col, Form, Input, Modal, Row, Select, Space, Typography} from 'antd';
|
||||||
import { PlusOutlined, MinusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
import {PlusOutlined, MinusOutlined} from '@ant-design/icons';
|
||||||
import { InputAddon } from '@/components/ui';
|
import {InputAddon} from '@/components/ui';
|
||||||
import { RuleFormSchema, type RuleFormValues } from '@/schemas/xray';
|
import {RuleFormSchema, type RuleFormValues} from '@/schemas/xray';
|
||||||
|
import {LabelWithOnePerLineTooltip, LabelWithTooltip} from "@/components/ui/TooltipsHelper";
|
||||||
|
|
||||||
export interface RoutingRule {
|
export interface RoutingRule {
|
||||||
type?: string;
|
type?: string;
|
||||||
|
|
@ -20,6 +21,7 @@ export interface RoutingRule {
|
||||||
attrs?: Record<string, string>;
|
attrs?: Record<string, string>;
|
||||||
outboundTag?: string;
|
outboundTag?: string;
|
||||||
balancerTag?: string;
|
balancerTag?: string;
|
||||||
|
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,6 +61,30 @@ function csv(value: string): string[] {
|
||||||
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CommaSeparatedTextArea = ({value, onChange, placeholder}: {
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}) => {
|
||||||
|
const displayValue = value ? value.split(',').join('\n') : '';
|
||||||
|
|
||||||
|
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const commaSeparated = e.target.value
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.join(',');
|
||||||
|
onChange(commaSeparated);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input.TextArea
|
||||||
|
autoSize={{minRows: 2, maxRows: 10}}
|
||||||
|
value={displayValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function RuleFormModal({
|
export default function RuleFormModal({
|
||||||
open,
|
open,
|
||||||
rule,
|
rule,
|
||||||
|
|
@ -68,10 +94,10 @@ export default function RuleFormModal({
|
||||||
onClose,
|
onClose,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}: RuleFormModalProps) {
|
}: RuleFormModalProps) {
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
const [form, setForm] = useState<FormState>(initialForm);
|
const [form, setForm] = useState<FormState>(initialForm);
|
||||||
const isEdit = rule != null;
|
const isEdit = rule != null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
if (rule) {
|
if (rule) {
|
||||||
|
|
@ -94,10 +120,10 @@ export default function RuleFormModal({
|
||||||
setForm(initialForm());
|
setForm(initialForm());
|
||||||
}
|
}
|
||||||
}, [open, rule]);
|
}, [open, rule]);
|
||||||
|
|
||||||
const update = <K extends keyof FormState>(key: K, value: FormState[K]) =>
|
const update = <K extends keyof FormState>(key: K, value: FormState[K]) =>
|
||||||
setForm((prev) => ({ ...prev, [key]: value }));
|
setForm((prev) => ({...prev, [key]: value}));
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
const validated = RuleFormSchema.safeParse(form);
|
const validated = RuleFormSchema.safeParse(form);
|
||||||
if (!validated.success) return;
|
if (!validated.success) return;
|
||||||
|
|
@ -128,173 +154,216 @@ export default function RuleFormModal({
|
||||||
}
|
}
|
||||||
onConfirm(out);
|
onConfirm(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = isEdit
|
const title = isEdit
|
||||||
? `${t('edit')} ${t('pages.xray.Routings')}`
|
? `${t('edit')} ${t('pages.xray.Routings')}`
|
||||||
: `+ ${t('pages.xray.Routings')}`;
|
: `+ ${t('pages.xray.Routings')}`;
|
||||||
const okText = isEdit ? t('pages.clients.submitEdit') : t('create');
|
const okText = isEdit ? t('pages.clients.submitEdit') : t('create');
|
||||||
|
|
||||||
|
const rowLayout = {gutter: 16};
|
||||||
|
const colLayout = {xs: 24, md: 8};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={open}
|
open={open}
|
||||||
title={title}
|
title={title}
|
||||||
okText={okText}
|
okText={okText}
|
||||||
cancelText={t('close')}
|
cancelText={t('close')}
|
||||||
mask={{ closable: false }}
|
mask={{closable: false}}
|
||||||
width={640}
|
width={1400}
|
||||||
onOk={submit}
|
onOk={submit}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
|
style={{top: 20}}
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
maxHeight: 'calc(100vh - 160px)',
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '8px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Form colon={false} labelCol={{ md: { span: 8 } }} wrapperCol={{ md: { span: 14 } }}>
|
<Form layout="vertical" colon={false}>
|
||||||
<Form.Item
|
<Row {...rowLayout}>
|
||||||
label={
|
<Col {...colLayout}>
|
||||||
<Tooltip title={t('pages.xray.rules.useComma')}>
|
<Form.Item label={<LabelWithOnePerLineTooltip labelKey="pages.xray.ruleForm.sourceIps"/>}>
|
||||||
{t('pages.xray.ruleForm.sourceIps')} <QuestionCircleOutlined />
|
<CommaSeparatedTextArea
|
||||||
</Tooltip>
|
value={form.sourceIP}
|
||||||
}
|
onChange={(v) => update('sourceIP', v)}
|
||||||
>
|
placeholder={"0.0.0.0/8\nfc00::/7\ngeoip:ir"}
|
||||||
<Input value={form.sourceIP} onChange={(e) => update('sourceIP', e.target.value)} placeholder="0.0.0.0/8, fc00::/7, geoip:ir" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={
|
|
||||||
<Tooltip title={t('pages.xray.rules.useComma')}>
|
|
||||||
{t('pages.xray.ruleForm.sourcePort')} <QuestionCircleOutlined />
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Input value={form.sourcePort} onChange={(e) => update('sourcePort', e.target.value)} placeholder="53,443,1000-2000" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={
|
|
||||||
<Tooltip title={t('pages.xray.rules.useComma')}>
|
|
||||||
{t('pages.xray.ruleForm.vlessRoute')} <QuestionCircleOutlined />
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Input value={form.vlessRoute} onChange={(e) => update('vlessRoute', e.target.value)} placeholder="53,443,1000-2000" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item label={t('pages.inbounds.network')}>
|
|
||||||
<Select
|
|
||||||
value={form.network}
|
|
||||||
onChange={(v) => update('network', v)}
|
|
||||||
options={NETWORKS.map((n) => ({ value: n, label: n || '(any)' }))}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item label={t('pages.inbounds.protocol')}>
|
|
||||||
<Select
|
|
||||||
mode="multiple"
|
|
||||||
value={form.protocol}
|
|
||||||
onChange={(v) => update('protocol', v)}
|
|
||||||
options={PROTOCOLS.map((p) => ({ value: p, label: p }))}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item label={t('pages.xray.ruleForm.attributes')}>
|
|
||||||
<Button size="small" icon={<PlusOutlined />} onClick={() => update('attrs', [...form.attrs, ['', '']])} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item wrapperCol={{ span: 24 }}>
|
|
||||||
{form.attrs.map((attr, idx) => (
|
|
||||||
<Space.Compact key={idx} block className="mb-8">
|
|
||||||
<InputAddon>{`${idx + 1}`}</InputAddon>
|
|
||||||
<Input
|
|
||||||
value={attr[0]}
|
|
||||||
placeholder={t('pages.nodes.name')}
|
|
||||||
onChange={(e) => {
|
|
||||||
const next = form.attrs.map((a, i) => (i === idx ? ([e.target.value, a[1]] as [string, string]) : a));
|
|
||||||
update('attrs', next);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
</Form.Item>
|
||||||
value={attr[1]}
|
</Col>
|
||||||
placeholder={t('pages.xray.ruleForm.value')}
|
|
||||||
onChange={(e) => {
|
<Col {...colLayout}>
|
||||||
const next = form.attrs.map((a, i) => (i === idx ? ([a[0], e.target.value] as [string, string]) : a));
|
<Form.Item label={<LabelWithOnePerLineTooltip labelKey="pages.xray.ruleForm.sourcePort"/>}>
|
||||||
update('attrs', next);
|
<CommaSeparatedTextArea
|
||||||
}}
|
value={form.sourcePort}
|
||||||
|
onChange={(v) => update('sourcePort', v)}
|
||||||
|
placeholder={"53\n443\n1000-2000"}
|
||||||
/>
|
/>
|
||||||
<Button
|
</Form.Item>
|
||||||
icon={<MinusOutlined />}
|
</Col>
|
||||||
onClick={() => update('attrs', form.attrs.filter((_, i) => i !== idx))}
|
|
||||||
|
<Col {...colLayout}>
|
||||||
|
<Form.Item label={<LabelWithOnePerLineTooltip labelKey="pages.xray.ruleForm.vlessRoute"/>}>
|
||||||
|
<CommaSeparatedTextArea
|
||||||
|
value={form.vlessRoute}
|
||||||
|
onChange={(v) => update('vlessRoute', v)}
|
||||||
|
placeholder={"53\n443\n1000-2000"}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Form.Item>
|
||||||
))}
|
</Col>
|
||||||
</Form.Item>
|
</Row>
|
||||||
|
|
||||||
<Form.Item
|
<Row {...rowLayout}>
|
||||||
label={
|
<Col {...colLayout}>
|
||||||
<Tooltip title={t('pages.xray.rules.useComma')}>
|
<Form.Item label={<LabelWithOnePerLineTooltip labelKey="pages.xray.ruleForm.user"/>}>
|
||||||
IP <QuestionCircleOutlined />
|
<CommaSeparatedTextArea
|
||||||
</Tooltip>
|
value={form.user}
|
||||||
}
|
onChange={(v) => update('user', v)}
|
||||||
>
|
placeholder="email address"
|
||||||
<Input value={form.ip} onChange={(e) => update('ip', e.target.value)} placeholder="0.0.0.0/8, fc00::/7, geoip:ir" />
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
<Form.Item
|
|
||||||
label={
|
<Col {...colLayout}>
|
||||||
<Tooltip title={t('pages.xray.rules.useComma')}>
|
<Form.Item label={t('pages.inbounds.network')}>
|
||||||
{t('domainName')} <QuestionCircleOutlined />
|
<Select
|
||||||
</Tooltip>
|
value={form.network}
|
||||||
}
|
onChange={(v) => update('network', v)}
|
||||||
>
|
options={NETWORKS.map((n) => ({value: n, label: n || '(any)'}))}
|
||||||
<Input value={form.domain} onChange={(e) => update('domain', e.target.value)} placeholder="google.com, geosite:cn" />
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
<Form.Item
|
|
||||||
label={
|
<Col {...colLayout}>
|
||||||
<Tooltip title={t('pages.xray.rules.useComma')}>
|
<Form.Item label={t('pages.inbounds.protocol')}>
|
||||||
{t('pages.xray.ruleForm.user')} <QuestionCircleOutlined />
|
<Select
|
||||||
</Tooltip>
|
mode="multiple"
|
||||||
}
|
value={form.protocol}
|
||||||
>
|
onChange={(v) => update('protocol', v)}
|
||||||
<Input value={form.user} onChange={(e) => update('user', e.target.value)} placeholder="email address" />
|
options={PROTOCOLS.map((p) => ({value: p, label: p}))}
|
||||||
</Form.Item>
|
/>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
</Col>
|
||||||
label={
|
</Row>
|
||||||
<Tooltip title={t('pages.xray.rules.useComma')}>
|
|
||||||
{t('pages.inbounds.port')} <QuestionCircleOutlined />
|
<Row {...rowLayout}>
|
||||||
</Tooltip>
|
<Col {...colLayout}>
|
||||||
}
|
<Form.Item label={t('pages.xray.ruleForm.inboundTags')}>
|
||||||
>
|
<Select
|
||||||
<Input value={form.port} onChange={(e) => update('port', e.target.value)} placeholder="53,443,1000-2000" />
|
mode="multiple"
|
||||||
</Form.Item>
|
value={form.inboundTag}
|
||||||
|
onChange={(v) => update('inboundTag', v)}
|
||||||
<Form.Item label={t('pages.xray.ruleForm.inboundTags')}>
|
options={inboundTags.map((tag) => ({value: tag, label: tag}))}
|
||||||
<Select
|
/>
|
||||||
mode="multiple"
|
</Form.Item>
|
||||||
value={form.inboundTag}
|
</Col>
|
||||||
onChange={(v) => update('inboundTag', v)}
|
|
||||||
options={inboundTags.map((tag) => ({ value: tag, label: tag }))}
|
<Col {...colLayout}>
|
||||||
/>
|
<Form.Item label={t('pages.xray.ruleForm.outboundTag')}>
|
||||||
</Form.Item>
|
<Select
|
||||||
|
value={form.outboundTag}
|
||||||
<Form.Item label={t('pages.xray.ruleForm.outboundTag')}>
|
onChange={(v) => update('outboundTag', v)}
|
||||||
<Select
|
options={outboundTags.map((tag) => ({value: tag, label: tag || '(none)'}))}
|
||||||
value={form.outboundTag}
|
/>
|
||||||
onChange={(v) => update('outboundTag', v)}
|
</Form.Item>
|
||||||
options={outboundTags.map((tag) => ({ value: tag, label: tag || '(none)' }))}
|
</Col>
|
||||||
/>
|
|
||||||
</Form.Item>
|
<Col {...colLayout}>
|
||||||
|
<Form.Item
|
||||||
<Form.Item
|
label={
|
||||||
label={
|
<LabelWithTooltip
|
||||||
<Tooltip title={t('pages.xray.ruleForm.balancerTagTooltip')}>
|
labelKey="pages.xray.ruleForm.balancerTag"
|
||||||
{t('pages.xray.ruleForm.balancerTag')} <QuestionCircleOutlined />
|
tooltipKey="pages.xray.ruleForm.balancerTagTooltip"
|
||||||
</Tooltip>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
value={form.balancerTag}
|
value={form.balancerTag}
|
||||||
onChange={(v) => update('balancerTag', v)}
|
onChange={(v) => update('balancerTag', v)}
|
||||||
options={balancerTags.map((tag) => ({ value: tag, label: tag || '(none)' }))}
|
options={balancerTags.map((tag) => ({value: tag, label: tag || '(none)'}))}
|
||||||
/>
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row {...rowLayout}>
|
||||||
|
<Col {...colLayout}>
|
||||||
|
<Form.Item label={<LabelWithOnePerLineTooltip labelKey="IP"/>}>
|
||||||
|
<CommaSeparatedTextArea
|
||||||
|
value={form.ip}
|
||||||
|
onChange={(v) => update('ip', v)}
|
||||||
|
placeholder={`0.0.0.0/8\nfc00::/7\ngeoip:ir`}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col {...colLayout}>
|
||||||
|
<Form.Item label={<LabelWithOnePerLineTooltip labelKey="pages.inbounds.port"/>}>
|
||||||
|
<CommaSeparatedTextArea
|
||||||
|
value={form.port}
|
||||||
|
onChange={(v) => update('port', v)}
|
||||||
|
placeholder={`53\n443\n1000-2000`}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col {...colLayout}>
|
||||||
|
<Form.Item label={<LabelWithOnePerLineTooltip labelKey="domainName"/>}>
|
||||||
|
<CommaSeparatedTextArea
|
||||||
|
value={form.domain}
|
||||||
|
onChange={(v) => update('domain', v)}
|
||||||
|
placeholder={`google.com\ngeosite:cn`}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Space orientation="horizontal">
|
||||||
|
<Typography.Text>
|
||||||
|
{t('pages.xray.ruleForm.attributes')}
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<PlusOutlined/>}
|
||||||
|
onClick={() => update('attrs', [...form.attrs, ['', '']])}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
{form.attrs.length > 0 && (
|
||||||
|
<Form.Item>
|
||||||
|
{form.attrs.map((attr, idx) => (
|
||||||
|
<Space.Compact key={idx} block className="mb-8">
|
||||||
|
<InputAddon>{`${idx + 1}`}</InputAddon>
|
||||||
|
<Input
|
||||||
|
value={attr[0]}
|
||||||
|
placeholder={t('pages.nodes.name')}
|
||||||
|
onChange={(e) => {
|
||||||
|
const next = form.attrs.map((a, i) => (i === idx ? ([e.target.value, a[1]] as [string, string]) : a));
|
||||||
|
update('attrs', next);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={attr[1]}
|
||||||
|
placeholder={t('pages.xray.ruleForm.value')}
|
||||||
|
onChange={(e) => {
|
||||||
|
const next = form.attrs.map((a, i) => (i === idx ? ([a[0], e.target.value] as [string, string]) : a));
|
||||||
|
update('attrs', next);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={<MinusOutlined/>}
|
||||||
|
onClick={() => update('attrs', form.attrs.filter((_, i) => i !== idx))}
|
||||||
|
/>
|
||||||
|
</Space.Compact>
|
||||||
|
))}
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1171,7 +1171,8 @@
|
||||||
"info": "معلومات",
|
"info": "معلومات",
|
||||||
"add": "أضف قاعدة",
|
"add": "أضف قاعدة",
|
||||||
"edit": "عدل القاعدة",
|
"edit": "عدل القاعدة",
|
||||||
"useComma": "عناصر مفصولة بفواصل"
|
"useComma": "عناصر مفصولة بفواصل",
|
||||||
|
"onePerLine": "عنصر واحد لكل سطر"
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
"dragToReorder": "اسحب لإعادة الترتيب"
|
"dragToReorder": "اسحب لإعادة الترتيب"
|
||||||
|
|
|
||||||
|
|
@ -1171,7 +1171,8 @@
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
"add": "Add Rule",
|
"add": "Add Rule",
|
||||||
"edit": "Edit Rule",
|
"edit": "Edit Rule",
|
||||||
"useComma": "Comma-separated list"
|
"useComma": "Comma-separated list",
|
||||||
|
"onePerLine": "One item per line"
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
"dragToReorder": "Drag to reorder"
|
"dragToReorder": "Drag to reorder"
|
||||||
|
|
|
||||||
|
|
@ -1171,7 +1171,8 @@
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
"add": "Agregar Regla",
|
"add": "Agregar Regla",
|
||||||
"edit": "Editar Regla",
|
"edit": "Editar Regla",
|
||||||
"useComma": "Elementos separados por comas"
|
"useComma": "Elementos separados por comas",
|
||||||
|
"onePerLine": "Un elemento por línea"
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
"dragToReorder": "Arrastra para reordenar"
|
"dragToReorder": "Arrastra para reordenar"
|
||||||
|
|
|
||||||
|
|
@ -1171,7 +1171,8 @@
|
||||||
"info": "اطلاعات",
|
"info": "اطلاعات",
|
||||||
"add": "افزودن قانون",
|
"add": "افزودن قانون",
|
||||||
"edit": "ویرایش قانون",
|
"edit": "ویرایش قانون",
|
||||||
"useComma": "موارد جدا شده با کاما"
|
"useComma": "موارد جدا شده با کاما",
|
||||||
|
"onePerLine": "یک مورد در هر خط"
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
"dragToReorder": "برای تغییر ترتیب بکشید"
|
"dragToReorder": "برای تغییر ترتیب بکشید"
|
||||||
|
|
|
||||||
|
|
@ -1171,7 +1171,8 @@
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
"add": "Tambahkan Aturan",
|
"add": "Tambahkan Aturan",
|
||||||
"edit": "Edit Aturan",
|
"edit": "Edit Aturan",
|
||||||
"useComma": "Item yang dipisahkan koma"
|
"useComma": "Item yang dipisahkan koma",
|
||||||
|
"onePerLine": "Satu item per baris"
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
"dragToReorder": "Seret untuk mengurutkan ulang"
|
"dragToReorder": "Seret untuk mengurutkan ulang"
|
||||||
|
|
|
||||||
|
|
@ -1171,7 +1171,8 @@
|
||||||
"info": "情報",
|
"info": "情報",
|
||||||
"add": "ルール追加",
|
"add": "ルール追加",
|
||||||
"edit": "ルール編集",
|
"edit": "ルール編集",
|
||||||
"useComma": "カンマ区切りの項目"
|
"useComma": "カンマ区切りの項目",
|
||||||
|
"onePerLine": "1行につき1項目"
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
"dragToReorder": "ドラッグして並べ替え"
|
"dragToReorder": "ドラッグして並べ替え"
|
||||||
|
|
|
||||||
|
|
@ -1171,7 +1171,8 @@
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
"add": "Adicionar Regra",
|
"add": "Adicionar Regra",
|
||||||
"edit": "Editar Regra",
|
"edit": "Editar Regra",
|
||||||
"useComma": "Itens separados por vírgula"
|
"useComma": "Itens separados por vírgula",
|
||||||
|
"onePerLine": "Um item por linha"
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
"dragToReorder": "Arraste para reordenar"
|
"dragToReorder": "Arraste para reordenar"
|
||||||
|
|
|
||||||
|
|
@ -1171,7 +1171,8 @@
|
||||||
"info": "Инфо",
|
"info": "Инфо",
|
||||||
"add": "Создать правило",
|
"add": "Создать правило",
|
||||||
"edit": "Редактировать правило",
|
"edit": "Редактировать правило",
|
||||||
"useComma": "Элементы, разделённые запятыми"
|
"useComma": "Элементы, разделённые запятыми",
|
||||||
|
"onePerLine": "Один элемент на строку"
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
"dragToReorder": "Перетащите для изменения порядка"
|
"dragToReorder": "Перетащите для изменения порядка"
|
||||||
|
|
|
||||||
|
|
@ -1171,7 +1171,8 @@
|
||||||
"info": "Bilgi",
|
"info": "Bilgi",
|
||||||
"add": "Kural Ekle",
|
"add": "Kural Ekle",
|
||||||
"edit": "Kuralı Düzenle",
|
"edit": "Kuralı Düzenle",
|
||||||
"useComma": "Virgülle ayrılmış öğeler"
|
"useComma": "Virgülle ayrılmış öğeler",
|
||||||
|
"onePerLine": "Her satırda bir öğe"
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
"dragToReorder": "Yeniden sıralamak için sürükleyin"
|
"dragToReorder": "Yeniden sıralamak için sürükleyin"
|
||||||
|
|
|
||||||
|
|
@ -1171,7 +1171,8 @@
|
||||||
"info": "Інфо",
|
"info": "Інфо",
|
||||||
"add": "Додати правило",
|
"add": "Додати правило",
|
||||||
"edit": "Редагувати правило",
|
"edit": "Редагувати правило",
|
||||||
"useComma": "Елементи, розділені комами"
|
"useComma": "Елементи, розділені комами",
|
||||||
|
"onePerLine": "Один елемент на рядок"
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
"dragToReorder": "Перетягніть для зміни порядку"
|
"dragToReorder": "Перетягніть для зміни порядку"
|
||||||
|
|
|
||||||
|
|
@ -1171,7 +1171,8 @@
|
||||||
"info": "Thông tin",
|
"info": "Thông tin",
|
||||||
"add": "Thêm quy tắc",
|
"add": "Thêm quy tắc",
|
||||||
"edit": "Chỉnh sửa quy tắc",
|
"edit": "Chỉnh sửa quy tắc",
|
||||||
"useComma": "Các mục được phân tách bằng dấu phẩy"
|
"useComma": "Các mục được phân tách bằng dấu phẩy",
|
||||||
|
"onePerLine": "Một mục trên mỗi dòng"
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
"dragToReorder": "Kéo để sắp xếp lại"
|
"dragToReorder": "Kéo để sắp xếp lại"
|
||||||
|
|
|
||||||
|
|
@ -1171,7 +1171,8 @@
|
||||||
"info": "信息",
|
"info": "信息",
|
||||||
"add": "添加规则",
|
"add": "添加规则",
|
||||||
"edit": "编辑规则",
|
"edit": "编辑规则",
|
||||||
"useComma": "逗号分隔的项目"
|
"useComma": "逗号分隔的项目",
|
||||||
|
"onePerLine": "每行一件"
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
"dragToReorder": "拖动以重新排序"
|
"dragToReorder": "拖动以重新排序"
|
||||||
|
|
|
||||||
|
|
@ -1171,7 +1171,8 @@
|
||||||
"info": "資訊",
|
"info": "資訊",
|
||||||
"add": "新增規則",
|
"add": "新增規則",
|
||||||
"edit": "編輯規則",
|
"edit": "編輯規則",
|
||||||
"useComma": "逗號分隔的項目"
|
"useComma": "逗號分隔的項目",
|
||||||
|
"onePerLine": "每行一件"
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
"dragToReorder": "拖曳以重新排序"
|
"dragToReorder": "拖曳以重新排序"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue