diff --git a/Changelog.md b/Changelog.md
index 14d7625ae..2cf5cabd1 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -51,6 +51,7 @@
✨ 新增功能
- 新增连接页面按网络协议(TCP/UDP)筛选连接
+- 新增从连接详情快捷设置域名或进程规则,支持子域匹配与修改已有规则
diff --git a/src/components/connection/connection-detail.tsx b/src/components/connection/connection-detail.tsx
index 4a20aa149..0bd9bea9a 100644
--- a/src/components/connection/connection-detail.tsx
+++ b/src/components/connection/connection-detail.tsx
@@ -5,8 +5,15 @@ import { useCallback, useImperativeHandle, useState, type Ref } from 'react'
import { useTranslation } from 'react-i18next'
import { closeConnection } from 'tauri-plugin-mihomo-api'
+import { useProfiles } from '@/hooks/use-profiles'
import parseTraffic from '@/utils/parse-traffic'
+import { getConnectionRuleTargets } from './connection-rule'
+import {
+ ConnectionRuleDialog,
+ type ConnectionRuleTarget,
+} from './connection-rule-dialog'
+
export interface ConnectionDetailRef {
open: (detail: IConnectionsItem, closed: boolean) => void
close: () => void
@@ -17,6 +24,9 @@ export function ConnectionDetail({ ref }: { ref?: Ref }) {
const [detail, setDetail] = useState(null)
const [closed, setClosed] = useState(false)
const theme = useTheme()
+ const [ruleTarget, setRuleTarget] = useState(
+ null,
+ )
const onClose = useCallback(() => {
setOpen(false)
@@ -35,32 +45,44 @@ export function ConnectionDetail({ ref }: { ref?: Ref }) {
}))
return (
-
- ) : null
- }
- />
+ <>
+ {
+ setRuleTarget(target)
+ onClose()
+ }}
+ />
+ ) : null
+ }
+ />
+ {ruleTarget && (
+ setRuleTarget(null)}
+ />
+ )}
+ >
)
}
@@ -68,11 +90,24 @@ interface InnerProps {
data: IConnectionsItem
closed: boolean
onClose?: () => void
+ onAddRule: (target: ConnectionRuleTarget) => void
}
-const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
+const InnerConnectionDetail = ({
+ data,
+ closed,
+ onClose,
+ onAddRule,
+}: InnerProps) => {
const { t } = useTranslation()
+ const { current } = useProfiles()
const { metadata, rulePayload } = data
+ const targets = getConnectionRuleTargets(metadata)
+ const canAddRule = Boolean(
+ current?.uid &&
+ current.option?.rules &&
+ (targets.domain || targets.process),
+ )
const theme = useTheme()
const chains = [...data.chains].reverse().join(' / ')
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule
@@ -108,7 +143,7 @@ const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
{ label: t('connections.components.fields.rule'), value: rule },
{
label: t('connections.components.fields.process'),
- value: `${metadata.process}${metadata.processPath ? `(${metadata.processPath})` : ''}`,
+ value: `${metadata.process ?? ''}${metadata.processPath ? `(${metadata.processPath})` : ''}`,
},
{
label: t('connections.components.fields.time'),
@@ -157,16 +192,31 @@ const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
))}
- {!closed && (
-
+
+ {!closed && (
+ )}
+
+ {!canAddRule && (
+
+ {t(
+ targets.domain || targets.process
+ ? 'connections.ruleDialog.noProfile'
+ : 'connections.ruleDialog.noMetadata',
+ )}
)}
diff --git a/src/components/connection/connection-rule-dialog.tsx b/src/components/connection/connection-rule-dialog.tsx
new file mode 100644
index 000000000..7e22f11d1
--- /dev/null
+++ b/src/components/connection/connection-rule-dialog.tsx
@@ -0,0 +1,391 @@
+import {
+ Alert,
+ Autocomplete,
+ Box,
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ FormControlLabel,
+ MenuItem,
+ Radio,
+ RadioGroup,
+ TextField,
+ Typography,
+} from '@mui/material'
+import { useLockFn } from 'ahooks'
+import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+
+import {
+ useClashConfigData,
+ useProxiesData,
+} from '@/providers/app-data-context'
+import { getProfiles, readProfileFile, saveProfileFile } from '@/services/cmds'
+import { mutate } from '@/services/mutate'
+import { showNotice } from '@/services/notice-service'
+
+import {
+ getConnectionRuleTargets,
+ findConnectionRule,
+ saveConnectionRule,
+} from './connection-rule'
+
+export interface ConnectionRuleTarget {
+ metadata: IConnectionsItem['metadata']
+ profileUid: string
+ profileName: string
+ rulesUid: string
+}
+
+type ConnectionRuleKind = 'domain' | 'process'
+
+export function ConnectionRuleDialog({
+ target,
+ onClose,
+}: {
+ target: ConnectionRuleTarget
+ onClose: () => void
+}) {
+ const { t } = useTranslation()
+ const { proxyView } = useProxiesData()
+ const { clashConfig } = useClashConfigData()
+ const targets = getConnectionRuleTargets(target.metadata)
+ const [kind, setKind] = useState(
+ targets.domain ? 'domain' : 'process',
+ )
+ const [policy, setPolicy] = useState(null)
+ const [scope, setScope] = useState<'DOMAIN' | 'DOMAIN-SUFFIX'>('DOMAIN')
+ const [existing, setExisting] = useState
+ > | null>(null)
+ const [loadAttempt, setLoadAttempt] = useState(0)
+ const [loading, setLoading] = useState(true)
+ const [saving, setSaving] = useState(false)
+ const [error, setError] = useState('')
+ const selected = targets[kind]
+ const modifying = Boolean(
+ existing?.[
+ kind === 'domain' && scope === 'DOMAIN-SUFFIX' ? 'suffix' : kind
+ ],
+ )
+ const groups = proxyView
+ ? proxyView.groups.length || !proxyView.global
+ ? proxyView.groups
+ : [proxyView.global]
+ : []
+ const policies = [
+ ...new Set(['DIRECT', 'REJECT', ...groups.map((group) => group.name)]),
+ ]
+
+ const kindLabels = {
+ domain: t('connections.ruleDialog.domain'),
+ process: t('connections.components.fields.process'),
+ }
+
+ const unavailablePolicy = Boolean(policy && !policies.includes(policy))
+ const blocked = loading || saving || !existing
+
+ useEffect(() => {
+ let cancelled = false
+ readProfileFile(target.rulesUid)
+ .then((content) => {
+ const ruleTargets = getConnectionRuleTargets(target.metadata)
+ const rules = {
+ domain: ruleTargets.domain
+ ? findConnectionRule(content, ruleTargets.domain)
+ : undefined,
+ suffix: ruleTargets.domain
+ ? findConnectionRule(content, {
+ ...ruleTargets.domain,
+ type: 'DOMAIN-SUFFIX',
+ })
+ : undefined,
+ process: ruleTargets.process
+ ? findConnectionRule(content, ruleTargets.process)
+ : undefined,
+ }
+ if (cancelled) return
+ const initialKind = ruleTargets.domain ? 'domain' : 'process'
+ const initial =
+ initialKind === 'domain'
+ ? (rules.domain ?? rules.suffix)
+ : rules[initialKind]
+ setExisting(rules)
+ setPolicy(initial?.policy ?? null)
+ setScope(initial?.type === 'DOMAIN-SUFFIX' ? 'DOMAIN-SUFFIX' : 'DOMAIN')
+ })
+ .catch((err: unknown) => {
+ if (!cancelled)
+ setError(err instanceof Error ? err.message : String(err))
+ })
+ .finally(() => {
+ if (!cancelled) setLoading(false)
+ })
+ return () => {
+ cancelled = true
+ }
+ }, [target, loadAttempt])
+
+ const handleSave = useLockFn(async () => {
+ if (blocked || !selected || !policy || !policies.includes(policy)) return
+ if (/[\r\n,]/u.test(selected.value) || /[\r\n,]/u.test(policy)) {
+ setError(t('connections.ruleDialog.unsupportedValue'))
+ return
+ }
+ setSaving(true)
+ setError('')
+ try {
+ const result = await mutate(
+ async () => {
+ const checkProfile = async () => {
+ const profiles = await getProfiles()
+ const current = profiles.items?.find(
+ (item) => item.uid === profiles.current,
+ )
+ if (
+ profiles.current !== target.profileUid ||
+ current?.option?.rules !== target.rulesUid
+ ) {
+ throw new Error(t('connections.ruleDialog.profileChanged'))
+ }
+ }
+ await checkProfile()
+ const content = await readProfileFile(target.rulesUid)
+ const updated = saveConnectionRule(
+ content,
+ { ...selected, type: kind === 'domain' ? scope : selected.type },
+ policy,
+ )
+ await checkProfile()
+ return saveProfileFile(target.rulesUid, updated)
+ },
+ { id: `save-profile-file:${target.rulesUid}`, errorNotice: false },
+ )
+ if (!result.ok || !result.value) {
+ setError(t('connections.ruleDialog.applyFailed'))
+ return
+ }
+ showNotice.success('connections.ruleDialog.saved')
+ onClose()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : String(err))
+ } finally {
+ setSaving(false)
+ }
+ })
+
+ return (
+
+ )
+}
diff --git a/src/components/connection/connection-rule.ts b/src/components/connection/connection-rule.ts
new file mode 100644
index 000000000..1b42598dd
--- /dev/null
+++ b/src/components/connection/connection-rule.ts
@@ -0,0 +1,102 @@
+import { dump, load } from 'js-yaml'
+import isFQDN from 'validator/es/lib/isFQDN'
+import isIP from 'validator/es/lib/isIP'
+
+export function getConnectionRuleTargets(
+ metadata: Pick<
+ IConnectionsItem['metadata'],
+ 'host' | 'process' | 'processPath'
+ >,
+) {
+ const host = metadata.host?.trim() ?? ''
+ const path = metadata.processPath
+ const name = metadata.process
+ const domain =
+ host && !isIP(host) && isFQDN(host, { require_tld: false })
+ ? { type: 'DOMAIN', value: host }
+ : undefined
+ const process = path?.trim()
+ ? { type: 'PROCESS-PATH', value: path }
+ : name?.trim()
+ ? { type: 'PROCESS-NAME', value: name }
+ : undefined
+ return { domain, process }
+}
+
+function parseRuleExtension(content: string) {
+ const parsed: unknown = load(content)
+ if (
+ parsed !== undefined &&
+ parsed !== null &&
+ (typeof parsed !== 'object' ||
+ Array.isArray(parsed) ||
+ Object.getPrototypeOf(parsed) !== Object.prototype)
+ ) {
+ throw new Error('The rule extension must be a YAML mapping.')
+ }
+ const config = (parsed ?? {}) as Record
+ for (const key of ['prepend', 'append', 'delete']) {
+ const sequence = config[key]
+ if (
+ sequence != null &&
+ (!Array.isArray(sequence) ||
+ sequence.some((item) => typeof item !== 'string'))
+ ) {
+ throw new Error('The rule extension contains an invalid rule list.')
+ }
+ }
+ return config
+}
+
+interface RuleCondition {
+ type: string
+ value: string
+}
+
+function matchesConnectionRule(raw: string, target: RuleCondition) {
+ const parts = raw.split(',').map((part) => part.trim())
+ if (parts.length !== 3) return false
+ if (target.type === 'DOMAIN' || target.type === 'DOMAIN-SUFFIX') {
+ return (
+ parts[0] === target.type &&
+ parts[1].toLowerCase() === target.value.toLowerCase()
+ )
+ }
+ return parts[0] === target.type && parts[1] === target.value
+}
+
+export function findConnectionRule(content: string, target: RuleCondition) {
+ const config = parseRuleExtension(content)
+ for (const key of ['prepend', 'append']) {
+ for (const raw of (config[key] ?? []) as string[]) {
+ if (matchesConnectionRule(raw, target)) {
+ const [type, , policy] = raw.split(',').map((part) => part.trim())
+ return { type, policy }
+ }
+ }
+ }
+ return undefined
+}
+
+export function saveConnectionRule(
+ content: string,
+ target: RuleCondition,
+ policy: string,
+): string {
+ const config = parseRuleExtension(content)
+ const rule = `${target.type},${target.value},${policy}`
+ let replaced = false
+ for (const key of ['prepend', 'append']) {
+ if (!Array.isArray(config[key])) continue
+ config[key] = (config[key] as string[]).flatMap((raw) => {
+ if (!matchesConnectionRule(raw, target)) return [raw]
+ if (replaced) return []
+ replaced = true
+ return [rule]
+ })
+ }
+ if (!replaced) {
+ config.prepend = [rule, ...((config.prepend ?? []) as string[])]
+ }
+ return dump(config, { forceQuotes: true })
+}
diff --git a/src/locales/ar/connections.json b/src/locales/ar/connections.json
index c5bcd0057..370c63e79 100644
--- a/src/locales/ar/connections.json
+++ b/src/locales/ar/connections.json
@@ -30,5 +30,32 @@
"title": "الأعمدة",
"dragHandle": "Drag handle"
}
+ },
+ "ruleDialog": {
+ "title": "Add rule…",
+ "matchBy": "Match by",
+ "domain": "Domain",
+ "unavailable": "Not available",
+ "destination": "Add to the prepend rules of “{{name}}”.",
+ "newConnections": "New connections will match the updated configuration.",
+ "processPathHint": "Matches all destinations accessed by processes at this path.",
+ "processNameHint": "Matches all destinations accessed by processes with this name.",
+ "ruleModeHint": "Rule-based routing takes effect in Rule mode.",
+ "addAndApply": "Add and apply",
+ "profileChanged": "The active profile changed. Close this dialog and add the rule again.",
+ "applyFailed": "The rule could not be applied. Your selection has been kept.",
+ "unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
+ "saved": "Rule saved and configuration applied.",
+ "noMetadata": "This connection has no domain or process information for adding a rule.",
+ "noProfile": "No active profile with an editable rule extension is available.",
+ "entry": "Configure rule…",
+ "modifyTitle": "Modify rule",
+ "domainScope": "Domain scope",
+ "exactDomain": "Only this domain",
+ "allSubdomains": "This domain and all its subdomains",
+ "policyUnavailable": "This policy is no longer available. Choose another policy.",
+ "editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
+ "saveAndApply": "Save and apply",
+ "fullPath": "Full path"
}
}
diff --git a/src/locales/de/connections.json b/src/locales/de/connections.json
index 5f5b62dff..d62a076cc 100644
--- a/src/locales/de/connections.json
+++ b/src/locales/de/connections.json
@@ -30,5 +30,32 @@
"title": "Spalten",
"dragHandle": "Ziehpunkt"
}
+ },
+ "ruleDialog": {
+ "title": "Add rule…",
+ "matchBy": "Match by",
+ "domain": "Domain",
+ "unavailable": "Not available",
+ "destination": "Add to the prepend rules of “{{name}}”.",
+ "newConnections": "New connections will match the updated configuration.",
+ "processPathHint": "Matches all destinations accessed by processes at this path.",
+ "processNameHint": "Matches all destinations accessed by processes with this name.",
+ "ruleModeHint": "Rule-based routing takes effect in Rule mode.",
+ "addAndApply": "Add and apply",
+ "profileChanged": "The active profile changed. Close this dialog and add the rule again.",
+ "applyFailed": "The rule could not be applied. Your selection has been kept.",
+ "unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
+ "saved": "Rule saved and configuration applied.",
+ "noMetadata": "This connection has no domain or process information for adding a rule.",
+ "noProfile": "No active profile with an editable rule extension is available.",
+ "entry": "Configure rule…",
+ "modifyTitle": "Modify rule",
+ "domainScope": "Domain scope",
+ "exactDomain": "Only this domain",
+ "allSubdomains": "This domain and all its subdomains",
+ "policyUnavailable": "This policy is no longer available. Choose another policy.",
+ "editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
+ "saveAndApply": "Save and apply",
+ "fullPath": "Full path"
}
}
diff --git a/src/locales/en/connections.json b/src/locales/en/connections.json
index 82b3cebac..09b9854b5 100644
--- a/src/locales/en/connections.json
+++ b/src/locales/en/connections.json
@@ -30,5 +30,32 @@
"title": "Columns",
"dragHandle": "Drag handle"
}
+ },
+ "ruleDialog": {
+ "title": "Add rule…",
+ "matchBy": "Match by",
+ "domain": "Domain",
+ "unavailable": "Not available",
+ "destination": "Add to the prepend rules of “{{name}}”.",
+ "newConnections": "New connections will match the updated configuration.",
+ "processPathHint": "Matches all destinations accessed by processes at this path.",
+ "processNameHint": "Matches all destinations accessed by processes with this name.",
+ "ruleModeHint": "Rule-based routing takes effect in Rule mode.",
+ "addAndApply": "Add and apply",
+ "profileChanged": "The active profile changed. Close this dialog and add the rule again.",
+ "applyFailed": "The rule could not be applied. Your selection has been kept.",
+ "unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
+ "saved": "Rule saved and configuration applied.",
+ "noMetadata": "This connection has no domain or process information for adding a rule.",
+ "noProfile": "No active profile with an editable rule extension is available.",
+ "entry": "Configure rule…",
+ "modifyTitle": "Modify rule",
+ "domainScope": "Domain scope",
+ "exactDomain": "Only this domain",
+ "allSubdomains": "This domain and all its subdomains",
+ "policyUnavailable": "This policy is no longer available. Choose another policy.",
+ "editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
+ "saveAndApply": "Save and apply",
+ "fullPath": "Full path"
}
}
diff --git a/src/locales/es/connections.json b/src/locales/es/connections.json
index 1b49bc17c..ee8eef59c 100644
--- a/src/locales/es/connections.json
+++ b/src/locales/es/connections.json
@@ -30,5 +30,32 @@
"title": "Columnas",
"dragHandle": "Drag handle"
}
+ },
+ "ruleDialog": {
+ "title": "Add rule…",
+ "matchBy": "Match by",
+ "domain": "Domain",
+ "unavailable": "Not available",
+ "destination": "Add to the prepend rules of “{{name}}”.",
+ "newConnections": "New connections will match the updated configuration.",
+ "processPathHint": "Matches all destinations accessed by processes at this path.",
+ "processNameHint": "Matches all destinations accessed by processes with this name.",
+ "ruleModeHint": "Rule-based routing takes effect in Rule mode.",
+ "addAndApply": "Add and apply",
+ "profileChanged": "The active profile changed. Close this dialog and add the rule again.",
+ "applyFailed": "The rule could not be applied. Your selection has been kept.",
+ "unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
+ "saved": "Rule saved and configuration applied.",
+ "noMetadata": "This connection has no domain or process information for adding a rule.",
+ "noProfile": "No active profile with an editable rule extension is available.",
+ "entry": "Configure rule…",
+ "modifyTitle": "Modify rule",
+ "domainScope": "Domain scope",
+ "exactDomain": "Only this domain",
+ "allSubdomains": "This domain and all its subdomains",
+ "policyUnavailable": "This policy is no longer available. Choose another policy.",
+ "editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
+ "saveAndApply": "Save and apply",
+ "fullPath": "Full path"
}
}
diff --git a/src/locales/fa/connections.json b/src/locales/fa/connections.json
index a8c8687e7..7bd73b7c5 100644
--- a/src/locales/fa/connections.json
+++ b/src/locales/fa/connections.json
@@ -30,5 +30,32 @@
"title": "ستونها",
"dragHandle": "Drag handle"
}
+ },
+ "ruleDialog": {
+ "title": "Add rule…",
+ "matchBy": "Match by",
+ "domain": "Domain",
+ "unavailable": "Not available",
+ "destination": "Add to the prepend rules of “{{name}}”.",
+ "newConnections": "New connections will match the updated configuration.",
+ "processPathHint": "Matches all destinations accessed by processes at this path.",
+ "processNameHint": "Matches all destinations accessed by processes with this name.",
+ "ruleModeHint": "Rule-based routing takes effect in Rule mode.",
+ "addAndApply": "Add and apply",
+ "profileChanged": "The active profile changed. Close this dialog and add the rule again.",
+ "applyFailed": "The rule could not be applied. Your selection has been kept.",
+ "unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
+ "saved": "Rule saved and configuration applied.",
+ "noMetadata": "This connection has no domain or process information for adding a rule.",
+ "noProfile": "No active profile with an editable rule extension is available.",
+ "entry": "Configure rule…",
+ "modifyTitle": "Modify rule",
+ "domainScope": "Domain scope",
+ "exactDomain": "Only this domain",
+ "allSubdomains": "This domain and all its subdomains",
+ "policyUnavailable": "This policy is no longer available. Choose another policy.",
+ "editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
+ "saveAndApply": "Save and apply",
+ "fullPath": "Full path"
}
}
diff --git a/src/locales/id/connections.json b/src/locales/id/connections.json
index 29748ccf9..3757fc227 100644
--- a/src/locales/id/connections.json
+++ b/src/locales/id/connections.json
@@ -30,5 +30,32 @@
"title": "Kolom",
"dragHandle": "Drag handle"
}
+ },
+ "ruleDialog": {
+ "title": "Add rule…",
+ "matchBy": "Match by",
+ "domain": "Domain",
+ "unavailable": "Not available",
+ "destination": "Add to the prepend rules of “{{name}}”.",
+ "newConnections": "New connections will match the updated configuration.",
+ "processPathHint": "Matches all destinations accessed by processes at this path.",
+ "processNameHint": "Matches all destinations accessed by processes with this name.",
+ "ruleModeHint": "Rule-based routing takes effect in Rule mode.",
+ "addAndApply": "Add and apply",
+ "profileChanged": "The active profile changed. Close this dialog and add the rule again.",
+ "applyFailed": "The rule could not be applied. Your selection has been kept.",
+ "unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
+ "saved": "Rule saved and configuration applied.",
+ "noMetadata": "This connection has no domain or process information for adding a rule.",
+ "noProfile": "No active profile with an editable rule extension is available.",
+ "entry": "Configure rule…",
+ "modifyTitle": "Modify rule",
+ "domainScope": "Domain scope",
+ "exactDomain": "Only this domain",
+ "allSubdomains": "This domain and all its subdomains",
+ "policyUnavailable": "This policy is no longer available. Choose another policy.",
+ "editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
+ "saveAndApply": "Save and apply",
+ "fullPath": "Full path"
}
}
diff --git a/src/locales/jp/connections.json b/src/locales/jp/connections.json
index 25fb84e8a..c633e02d7 100644
--- a/src/locales/jp/connections.json
+++ b/src/locales/jp/connections.json
@@ -30,5 +30,32 @@
"title": "列",
"dragHandle": "Drag handle"
}
+ },
+ "ruleDialog": {
+ "title": "Add rule…",
+ "matchBy": "Match by",
+ "domain": "Domain",
+ "unavailable": "Not available",
+ "destination": "Add to the prepend rules of “{{name}}”.",
+ "newConnections": "New connections will match the updated configuration.",
+ "processPathHint": "Matches all destinations accessed by processes at this path.",
+ "processNameHint": "Matches all destinations accessed by processes with this name.",
+ "ruleModeHint": "Rule-based routing takes effect in Rule mode.",
+ "addAndApply": "Add and apply",
+ "profileChanged": "The active profile changed. Close this dialog and add the rule again.",
+ "applyFailed": "The rule could not be applied. Your selection has been kept.",
+ "unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
+ "saved": "Rule saved and configuration applied.",
+ "noMetadata": "This connection has no domain or process information for adding a rule.",
+ "noProfile": "No active profile with an editable rule extension is available.",
+ "entry": "Configure rule…",
+ "modifyTitle": "Modify rule",
+ "domainScope": "Domain scope",
+ "exactDomain": "Only this domain",
+ "allSubdomains": "This domain and all its subdomains",
+ "policyUnavailable": "This policy is no longer available. Choose another policy.",
+ "editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
+ "saveAndApply": "Save and apply",
+ "fullPath": "Full path"
}
}
diff --git a/src/locales/ko/connections.json b/src/locales/ko/connections.json
index 88173f6ac..235fb8916 100644
--- a/src/locales/ko/connections.json
+++ b/src/locales/ko/connections.json
@@ -30,5 +30,32 @@
"title": "열",
"dragHandle": "Drag handle"
}
+ },
+ "ruleDialog": {
+ "title": "Add rule…",
+ "matchBy": "Match by",
+ "domain": "Domain",
+ "unavailable": "Not available",
+ "destination": "Add to the prepend rules of “{{name}}”.",
+ "newConnections": "New connections will match the updated configuration.",
+ "processPathHint": "Matches all destinations accessed by processes at this path.",
+ "processNameHint": "Matches all destinations accessed by processes with this name.",
+ "ruleModeHint": "Rule-based routing takes effect in Rule mode.",
+ "addAndApply": "Add and apply",
+ "profileChanged": "The active profile changed. Close this dialog and add the rule again.",
+ "applyFailed": "The rule could not be applied. Your selection has been kept.",
+ "unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
+ "saved": "Rule saved and configuration applied.",
+ "noMetadata": "This connection has no domain or process information for adding a rule.",
+ "noProfile": "No active profile with an editable rule extension is available.",
+ "entry": "Configure rule…",
+ "modifyTitle": "Modify rule",
+ "domainScope": "Domain scope",
+ "exactDomain": "Only this domain",
+ "allSubdomains": "This domain and all its subdomains",
+ "policyUnavailable": "This policy is no longer available. Choose another policy.",
+ "editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
+ "saveAndApply": "Save and apply",
+ "fullPath": "Full path"
}
}
diff --git a/src/locales/pt/connections.json b/src/locales/pt/connections.json
index 8034e7599..25ec983c7 100644
--- a/src/locales/pt/connections.json
+++ b/src/locales/pt/connections.json
@@ -30,5 +30,32 @@
"title": "Colunas",
"dragHandle": "Alça de arrastar"
}
+ },
+ "ruleDialog": {
+ "title": "Add rule…",
+ "matchBy": "Match by",
+ "domain": "Domain",
+ "unavailable": "Not available",
+ "destination": "Add to the prepend rules of “{{name}}”.",
+ "newConnections": "New connections will match the updated configuration.",
+ "processPathHint": "Matches all destinations accessed by processes at this path.",
+ "processNameHint": "Matches all destinations accessed by processes with this name.",
+ "ruleModeHint": "Rule-based routing takes effect in Rule mode.",
+ "addAndApply": "Add and apply",
+ "profileChanged": "The active profile changed. Close this dialog and add the rule again.",
+ "applyFailed": "The rule could not be applied. Your selection has been kept.",
+ "unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
+ "saved": "Rule saved and configuration applied.",
+ "noMetadata": "This connection has no domain or process information for adding a rule.",
+ "noProfile": "No active profile with an editable rule extension is available.",
+ "entry": "Configure rule…",
+ "modifyTitle": "Modify rule",
+ "domainScope": "Domain scope",
+ "exactDomain": "Only this domain",
+ "allSubdomains": "This domain and all its subdomains",
+ "policyUnavailable": "This policy is no longer available. Choose another policy.",
+ "editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
+ "saveAndApply": "Save and apply",
+ "fullPath": "Full path"
}
}
diff --git a/src/locales/ru/connections.json b/src/locales/ru/connections.json
index 37b8adaef..da41ee087 100644
--- a/src/locales/ru/connections.json
+++ b/src/locales/ru/connections.json
@@ -30,5 +30,32 @@
"title": "Столбцы",
"dragHandle": "Маркер перетаскивания"
}
+ },
+ "ruleDialog": {
+ "title": "Add rule…",
+ "matchBy": "Match by",
+ "domain": "Domain",
+ "unavailable": "Not available",
+ "destination": "Add to the prepend rules of “{{name}}”.",
+ "newConnections": "New connections will match the updated configuration.",
+ "processPathHint": "Matches all destinations accessed by processes at this path.",
+ "processNameHint": "Matches all destinations accessed by processes with this name.",
+ "ruleModeHint": "Rule-based routing takes effect in Rule mode.",
+ "addAndApply": "Add and apply",
+ "profileChanged": "The active profile changed. Close this dialog and add the rule again.",
+ "applyFailed": "The rule could not be applied. Your selection has been kept.",
+ "unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
+ "saved": "Rule saved and configuration applied.",
+ "noMetadata": "This connection has no domain or process information for adding a rule.",
+ "noProfile": "No active profile with an editable rule extension is available.",
+ "entry": "Configure rule…",
+ "modifyTitle": "Modify rule",
+ "domainScope": "Domain scope",
+ "exactDomain": "Only this domain",
+ "allSubdomains": "This domain and all its subdomains",
+ "policyUnavailable": "This policy is no longer available. Choose another policy.",
+ "editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
+ "saveAndApply": "Save and apply",
+ "fullPath": "Full path"
}
}
diff --git a/src/locales/tr/connections.json b/src/locales/tr/connections.json
index b8f85292a..012e435ee 100644
--- a/src/locales/tr/connections.json
+++ b/src/locales/tr/connections.json
@@ -30,5 +30,32 @@
"title": "Sütunlar",
"dragHandle": "Sürükleme tutamacı"
}
+ },
+ "ruleDialog": {
+ "title": "Add rule…",
+ "matchBy": "Match by",
+ "domain": "Domain",
+ "unavailable": "Not available",
+ "destination": "Add to the prepend rules of “{{name}}”.",
+ "newConnections": "New connections will match the updated configuration.",
+ "processPathHint": "Matches all destinations accessed by processes at this path.",
+ "processNameHint": "Matches all destinations accessed by processes with this name.",
+ "ruleModeHint": "Rule-based routing takes effect in Rule mode.",
+ "addAndApply": "Add and apply",
+ "profileChanged": "The active profile changed. Close this dialog and add the rule again.",
+ "applyFailed": "The rule could not be applied. Your selection has been kept.",
+ "unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
+ "saved": "Rule saved and configuration applied.",
+ "noMetadata": "This connection has no domain or process information for adding a rule.",
+ "noProfile": "No active profile with an editable rule extension is available.",
+ "entry": "Configure rule…",
+ "modifyTitle": "Modify rule",
+ "domainScope": "Domain scope",
+ "exactDomain": "Only this domain",
+ "allSubdomains": "This domain and all its subdomains",
+ "policyUnavailable": "This policy is no longer available. Choose another policy.",
+ "editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
+ "saveAndApply": "Save and apply",
+ "fullPath": "Full path"
}
}
diff --git a/src/locales/tt/connections.json b/src/locales/tt/connections.json
index 48a28d4b8..cea7dada1 100644
--- a/src/locales/tt/connections.json
+++ b/src/locales/tt/connections.json
@@ -30,5 +30,32 @@
"title": "Баганалар",
"dragHandle": "Drag handle"
}
+ },
+ "ruleDialog": {
+ "title": "Add rule…",
+ "matchBy": "Match by",
+ "domain": "Domain",
+ "unavailable": "Not available",
+ "destination": "Add to the prepend rules of “{{name}}”.",
+ "newConnections": "New connections will match the updated configuration.",
+ "processPathHint": "Matches all destinations accessed by processes at this path.",
+ "processNameHint": "Matches all destinations accessed by processes with this name.",
+ "ruleModeHint": "Rule-based routing takes effect in Rule mode.",
+ "addAndApply": "Add and apply",
+ "profileChanged": "The active profile changed. Close this dialog and add the rule again.",
+ "applyFailed": "The rule could not be applied. Your selection has been kept.",
+ "unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
+ "saved": "Rule saved and configuration applied.",
+ "noMetadata": "This connection has no domain or process information for adding a rule.",
+ "noProfile": "No active profile with an editable rule extension is available.",
+ "entry": "Configure rule…",
+ "modifyTitle": "Modify rule",
+ "domainScope": "Domain scope",
+ "exactDomain": "Only this domain",
+ "allSubdomains": "This domain and all its subdomains",
+ "policyUnavailable": "This policy is no longer available. Choose another policy.",
+ "editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
+ "saveAndApply": "Save and apply",
+ "fullPath": "Full path"
}
}
diff --git a/src/locales/zh/connections.json b/src/locales/zh/connections.json
index 33a447813..94954042b 100644
--- a/src/locales/zh/connections.json
+++ b/src/locales/zh/connections.json
@@ -30,5 +30,32 @@
"title": "列设置",
"dragHandle": "拖拽控件"
}
+ },
+ "ruleDialog": {
+ "title": "添加规则…",
+ "matchBy": "匹配对象",
+ "domain": "域名",
+ "unavailable": "未获取",
+ "destination": "添加到「{{name}}」的前置规则。",
+ "newConnections": "新建立的连接将按更新后的配置匹配。",
+ "processPathHint": "匹配此路径对应进程访问的所有目标。",
+ "processNameHint": "匹配所有同名进程访问的所有目标。",
+ "ruleModeHint": "规则分流在规则模式下生效。",
+ "addAndApply": "添加并应用",
+ "profileChanged": "当前订阅已切换,请关闭弹窗后重新添加规则。",
+ "applyFailed": "规则未能应用,已保留当前选择。",
+ "unsupportedValue": "此内容包含快捷规则不支持的字符。",
+ "saved": "规则已保存,配置已应用。",
+ "noMetadata": "此连接没有可用于添加规则的域名或进程信息。",
+ "noProfile": "当前没有可编辑规则扩展的活动订阅。",
+ "entry": "设置规则…",
+ "modifyTitle": "修改规则",
+ "domainScope": "域名匹配范围",
+ "exactDomain": "仅此域名",
+ "allSubdomains": "此域名及所有子域名",
+ "policyUnavailable": "此策略已不可用,请选择其他策略。",
+ "editDestination": "修改「{{name}}」中的现有规则,保留排序。",
+ "saveAndApply": "保存并应用",
+ "fullPath": "完整路径"
}
}
diff --git a/src/locales/zhtw/connections.json b/src/locales/zhtw/connections.json
index 520f3a855..4b6c094ed 100644
--- a/src/locales/zhtw/connections.json
+++ b/src/locales/zhtw/connections.json
@@ -30,5 +30,32 @@
"title": "欄位設定",
"dragHandle": "拖曳以移動"
}
+ },
+ "ruleDialog": {
+ "title": "Add rule…",
+ "matchBy": "Match by",
+ "domain": "Domain",
+ "unavailable": "Not available",
+ "destination": "Add to the prepend rules of “{{name}}”.",
+ "newConnections": "New connections will match the updated configuration.",
+ "processPathHint": "Matches all destinations accessed by processes at this path.",
+ "processNameHint": "Matches all destinations accessed by processes with this name.",
+ "ruleModeHint": "Rule-based routing takes effect in Rule mode.",
+ "addAndApply": "Add and apply",
+ "profileChanged": "The active profile changed. Close this dialog and add the rule again.",
+ "applyFailed": "The rule could not be applied. Your selection has been kept.",
+ "unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
+ "saved": "Rule saved and configuration applied.",
+ "noMetadata": "This connection has no domain or process information for adding a rule.",
+ "noProfile": "No active profile with an editable rule extension is available.",
+ "entry": "Configure rule…",
+ "modifyTitle": "Modify rule",
+ "domainScope": "Domain scope",
+ "exactDomain": "Only this domain",
+ "allSubdomains": "This domain and all its subdomains",
+ "policyUnavailable": "This policy is no longer available. Choose another policy.",
+ "editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
+ "saveAndApply": "Save and apply",
+ "fullPath": "Full path"
}
}
diff --git a/src/types/generated/i18n-keys.ts b/src/types/generated/i18n-keys.ts
index ba6eac3a7..22a4ad9b2 100644
--- a/src/types/generated/i18n-keys.ts
+++ b/src/types/generated/i18n-keys.ts
@@ -22,6 +22,31 @@ export const translationKeys = [
'connections.components.actions.closeConnection',
'connections.components.columnManager.title',
'connections.components.columnManager.dragHandle',
+ 'connections.ruleDialog.title',
+ 'connections.ruleDialog.matchBy',
+ 'connections.ruleDialog.domain',
+ 'connections.ruleDialog.unavailable',
+ 'connections.ruleDialog.destination',
+ 'connections.ruleDialog.newConnections',
+ 'connections.ruleDialog.processPathHint',
+ 'connections.ruleDialog.processNameHint',
+ 'connections.ruleDialog.ruleModeHint',
+ 'connections.ruleDialog.addAndApply',
+ 'connections.ruleDialog.profileChanged',
+ 'connections.ruleDialog.applyFailed',
+ 'connections.ruleDialog.unsupportedValue',
+ 'connections.ruleDialog.saved',
+ 'connections.ruleDialog.noMetadata',
+ 'connections.ruleDialog.noProfile',
+ 'connections.ruleDialog.entry',
+ 'connections.ruleDialog.modifyTitle',
+ 'connections.ruleDialog.domainScope',
+ 'connections.ruleDialog.exactDomain',
+ 'connections.ruleDialog.allSubdomains',
+ 'connections.ruleDialog.policyUnavailable',
+ 'connections.ruleDialog.editDestination',
+ 'connections.ruleDialog.saveAndApply',
+ 'connections.ruleDialog.fullPath',
'home.page.tooltips.lightweightMode',
'home.page.tooltips.manual',
'home.page.tooltips.settings',
diff --git a/src/types/generated/i18n-resources.ts b/src/types/generated/i18n-resources.ts
index 3e5a527f9..a3c90b136 100644
--- a/src/types/generated/i18n-resources.ts
+++ b/src/types/generated/i18n-resources.ts
@@ -36,6 +36,33 @@ export interface TranslationResources {
page: {
title: string
}
+ ruleDialog: {
+ addAndApply: string
+ allSubdomains: string
+ applyFailed: string
+ destination: string
+ domain: string
+ domainScope: string
+ editDestination: string
+ entry: string
+ exactDomain: string
+ fullPath: string
+ matchBy: string
+ modifyTitle: string
+ newConnections: string
+ noMetadata: string
+ noProfile: string
+ policyUnavailable: string
+ processNameHint: string
+ processPathHint: string
+ profileChanged: string
+ ruleModeHint: string
+ saveAndApply: string
+ saved: string
+ title: string
+ unavailable: string
+ unsupportedValue: string
+ }
}
home: {
components: {