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 ( + + + {t( + modifying + ? 'connections.ruleDialog.modifyTitle' + : 'connections.ruleDialog.title', + )} + + + + { + const next = event.target.value as ConnectionRuleKind + const rule = + next === 'domain' + ? (existing?.domain ?? existing?.suffix) + : existing?.[next] + setKind(next) + setPolicy(rule?.policy ?? null) + setScope( + rule?.type === 'DOMAIN-SUFFIX' ? 'DOMAIN-SUFFIX' : 'DOMAIN', + ) + }} + > + {(['domain', 'process'] as const).map((option) => ( + + {kindLabels[option]} + {!targets[option] && + ` · ${t('connections.ruleDialog.unavailable')}`} + + ))} + + {kind === 'process' ? ( + <> + {target.metadata.process?.trim() && ( + + )} + {selected?.type === 'PROCESS-PATH' && ( + + )} + + ) : ( + + )} + + {kind === 'domain' && ( + { + setScope(value as 'DOMAIN' | 'DOMAIN-SUFFIX') + setPolicy( + existing?.[value === 'DOMAIN-SUFFIX' ? 'suffix' : 'domain'] + ?.policy ?? null, + ) + }} + sx={{ mt: 0.5 }} + > + } + disabled={blocked} + label={t('connections.ruleDialog.exactDomain')} + /> + } + disabled={blocked} + label={t('connections.ruleDialog.allSubdomains')} + /> + + )} + {kind === 'process' && ( + + {selected?.type === 'PROCESS-PATH' + ? t('connections.ruleDialog.processPathHint') + : t('connections.ruleDialog.processNameHint')} + + )} + + value === 'DIRECT' + ? t('proxies.components.enums.policies.DIRECT') + : value === 'REJECT' + ? t('proxies.components.enums.policies.REJECT') + : value + } + getOptionDisabled={(value) => !policies.includes(value)} + value={policy} + onChange={(_, value) => setPolicy(value)} + disabled={blocked} + loading={loading} + renderInput={(params) => ( + + )} + /> + {clashConfig && clashConfig.mode !== 'rule' && ( + + {t('connections.ruleDialog.ruleModeHint')} + + )} + {error && ( + + + {error} + {!existing && ( + + )} + + + )} + + {t( + modifying + ? 'connections.ruleDialog.editDestination' + : 'connections.ruleDialog.destination', + { + name: target.profileName, + }, + )} + + + {t('connections.ruleDialog.newConnections')} + + + + + + + + ) +} 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: {