diff --git a/scripts/cleanup-unused-i18n.mjs b/scripts/cleanup-unused-i18n.mjs index 7d463470..338d11b5 100644 --- a/scripts/cleanup-unused-i18n.mjs +++ b/scripts/cleanup-unused-i18n.mjs @@ -5,6 +5,8 @@ import path from "path"; import process from "process"; import { fileURLToPath } from "url"; +import ts from "typescript"; + const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -46,10 +48,44 @@ const SUPPORTED_EXTENSIONS = new Set([ ".mjs", ".cjs", ".vue", - ".rs", ".json", ]); +const TS_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]); + +const KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+)+$/; +const TEMPLATE_PREFIX_PATTERN = + /^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+)*\.$/; + +const IGNORED_KEY_PREFIXES = new Set([ + "text", + "primary", + "secondary", + "error", + "warning", + "success", + "info", + "background", + "grey", + "option", + "action", + "example", + "chrome", + "localhost", + "www", + "pac", + "V2", + "v2", + "v1", +]); + +const NOTICE_METHOD_NAMES = new Set(["success", "error", "info", "warning"]); +const NOTICE_SERVICE_IDENTIFIERS = new Set([ + "@/services/noticeService", + "./noticeService", + "../services/noticeService", +]); + const WHITELIST_KEYS = new Set([ "theme.light", "theme.dark", @@ -58,6 +94,8 @@ const WHITELIST_KEYS = new Set([ ]); const MAX_PREVIEW_ENTRIES = 40; +const dynamicKeyCache = new Map(); +const fileUsageCache = new Map(); function printUsage() { console.log(`Usage: pnpm node scripts/cleanup-unused-i18n.mjs [options] @@ -177,22 +215,37 @@ function getAllFiles(start, predicate) { return files; } -function loadSourceContents(sourceDirs) { - const sourceFiles = sourceDirs.flatMap((dir) => - getAllFiles( - dir, - (filePath) => - SUPPORTED_EXTENSIONS.has(path.extname(filePath)) && - !EXCLUDE_USAGE_DIRS.some((excluded) => - filePath.startsWith(`${excluded}${path.sep}`), - ) && - !EXCLUDE_USAGE_DIRS.includes(filePath), - ), - ); +function collectSourceFiles(sourceDirs) { + const seen = new Set(); + const files = []; - return sourceFiles - .map((filePath) => fs.readFileSync(filePath, "utf8")) - .join("\n"); + for (const dir of sourceDirs) { + const resolved = getAllFiles(dir, (filePath) => { + if (seen.has(filePath)) return false; + if (!SUPPORTED_EXTENSIONS.has(path.extname(filePath))) return false; + if ( + EXCLUDE_USAGE_DIRS.some((excluded) => + filePath.startsWith(`${excluded}${path.sep}`), + ) || + EXCLUDE_USAGE_DIRS.includes(filePath) + ) { + return false; + } + return true; + }); + + for (const filePath of resolved) { + seen.add(filePath); + files.push({ + path: filePath, + extension: path.extname(filePath).toLowerCase(), + content: fs.readFileSync(filePath, "utf8"), + }); + } + } + + files.sort((a, b) => a.path.localeCompare(b.path)); + return files; } function flattenLocale(obj, parent = "") { @@ -239,6 +292,411 @@ function diffLocaleKeys(baselineEntries, localeEntries) { return { missing, extra }; } +function determineScriptKind(extension) { + switch (extension) { + case ".ts": + return ts.ScriptKind.TS; + case ".tsx": + return ts.ScriptKind.TSX; + case ".jsx": + return ts.ScriptKind.JSX; + case ".js": + case ".mjs": + case ".cjs": + return ts.ScriptKind.JS; + default: + return ts.ScriptKind.TS; + } +} + +function getNamespaceFromKey(key) { + if (!key || typeof key !== "string") return null; + const [namespace] = key.split("."); + return namespace ?? null; +} + +function addTemplatePrefixCandidate( + prefix, + dynamicPrefixes, + baselineNamespaces, +) { + if (!prefix || typeof prefix !== "string") return; + const normalized = prefix.trim(); + if (!normalized) return; + let candidate = normalized; + if (!candidate.endsWith(".")) { + const lastDotIndex = candidate.lastIndexOf("."); + if (lastDotIndex === -1) { + return; + } + candidate = candidate.slice(0, lastDotIndex + 1); + } + if (!TEMPLATE_PREFIX_PATTERN.test(candidate)) return; + const namespace = getNamespaceFromKey(candidate); + if (!namespace || IGNORED_KEY_PREFIXES.has(namespace)) return; + if (!baselineNamespaces.has(namespace)) return; + dynamicPrefixes.add(candidate); +} + +function addKeyIfValid(key, usedKeys, baselineNamespaces, options = {}) { + if (!key || typeof key !== "string") return false; + if (!KEY_PATTERN.test(key)) return false; + const namespace = getNamespaceFromKey(key); + if (!namespace || IGNORED_KEY_PREFIXES.has(namespace)) return false; + if (!options.forceNamespace && !baselineNamespaces.has(namespace)) { + return false; + } + usedKeys.add(key); + return true; +} + +function collectImportSpecifiers(sourceFile) { + const specifiers = new Map(); + + sourceFile.forEachChild((node) => { + if (!ts.isImportDeclaration(node)) return; + const moduleText = + ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text; + if (!moduleText || !node.importClause) return; + + if (node.importClause.name) { + specifiers.set(node.importClause.name.text, moduleText); + } + + const { namedBindings } = node.importClause; + if (!namedBindings) return; + + if (ts.isNamespaceImport(namedBindings)) { + specifiers.set(namedBindings.name.text, `${moduleText}.*`); + return; + } + + if (ts.isNamedImports(namedBindings)) { + for (const element of namedBindings.elements) { + specifiers.set(element.name.text, moduleText); + } + } + }); + + return specifiers; +} + +function getCallExpressionChain(expression) { + const chain = []; + let current = expression; + while (current) { + if (ts.isIdentifier(current)) { + chain.unshift(current.text); + break; + } + if (ts.isPropertyAccessExpression(current)) { + chain.unshift(current.name.text); + current = current.expression; + continue; + } + if (ts.isElementAccessExpression(current)) { + const argument = current.argumentExpression; + if (ts.isStringLiteralLike(argument)) { + chain.unshift(argument.text); + current = current.expression; + continue; + } + return []; + } + break; + } + return chain; +} + +function classifyCallExpression(expression, importSpecifiers) { + if (!expression) return null; + const chain = getCallExpressionChain(expression); + if (chain.length === 0) return null; + + const last = chain[chain.length - 1]; + const root = chain[0]; + + if (last === "t") { + return { type: "translation", forceNamespace: true }; + } + + if ( + NOTICE_METHOD_NAMES.has(last) && + root === "showNotice" && + (!importSpecifiers || + NOTICE_SERVICE_IDENTIFIERS.has(importSpecifiers.get("showNotice") ?? "")) + ) { + return { type: "notice", forceNamespace: true }; + } + + return null; +} + +function resolveBindingValue(name, scopeStack) { + if (!name) return null; + for (let index = scopeStack.length - 1; index >= 0; index -= 1) { + const scope = scopeStack[index]; + if (scope && scope.has(name)) { + return scope.get(name); + } + } + return null; +} + +function resolveKeyFromExpression( + node, + scopeStack, + dynamicPrefixes, + baselineNamespaces, + importSpecifiers, +) { + if (!node) return null; + + if ( + ts.isStringLiteralLike(node) || + ts.isNoSubstitutionTemplateLiteral(node) + ) { + return node.text; + } + + if (ts.isTemplateExpression(node)) { + addTemplatePrefixCandidate( + node.head?.text ?? "", + dynamicPrefixes, + baselineNamespaces, + ); + for (const span of node.templateSpans) { + const literalText = span.literal?.text ?? ""; + const combined = (node.head?.text ?? "") + literalText; + addTemplatePrefixCandidate(combined, dynamicPrefixes, baselineNamespaces); + } + return null; + } + + if (ts.isBinaryExpression(node)) { + if (node.operatorToken.kind === ts.SyntaxKind.PlusToken) { + const left = resolveKeyFromExpression( + node.left, + scopeStack, + dynamicPrefixes, + baselineNamespaces, + importSpecifiers, + ); + const right = resolveKeyFromExpression( + node.right, + scopeStack, + dynamicPrefixes, + baselineNamespaces, + importSpecifiers, + ); + if (left && right) { + return `${left}${right}`; + } + if (left) { + addTemplatePrefixCandidate(left, dynamicPrefixes, baselineNamespaces); + } + return null; + } + return null; + } + + if (ts.isParenthesizedExpression(node)) { + return resolveKeyFromExpression( + node.expression, + scopeStack, + dynamicPrefixes, + baselineNamespaces, + importSpecifiers, + ); + } + + if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) { + return resolveKeyFromExpression( + node.expression, + scopeStack, + dynamicPrefixes, + baselineNamespaces, + importSpecifiers, + ); + } + + if (ts.isIdentifier(node)) { + return resolveBindingValue(node.text, scopeStack); + } + + if (ts.isCallExpression(node)) { + const classification = classifyCallExpression( + node.expression, + importSpecifiers, + ); + if (!classification) return null; + const firstArg = node.arguments[0]; + if (!firstArg) return null; + return resolveKeyFromExpression( + firstArg, + scopeStack, + dynamicPrefixes, + baselineNamespaces, + importSpecifiers, + ); + } + + return null; +} + +function collectUsedKeysFromTsFile( + file, + baselineNamespaces, + usedKeys, + dynamicPrefixes, +) { + let sourceFile; + + try { + sourceFile = ts.createSourceFile( + file.path, + file.content, + ts.ScriptTarget.Latest, + true, + determineScriptKind(file.extension), + ); + } catch (error) { + console.warn(`Warning: failed to parse ${file.path}: ${error.message}`); + return; + } + + const importSpecifiers = collectImportSpecifiers(sourceFile); + const scopeStack = [new Map()]; + + const visit = (node) => { + let scopePushed = false; + if ( + ts.isBlock(node) || + ts.isModuleBlock(node) || + node.kind === ts.SyntaxKind.CaseBlock || + ts.isCatchClause(node) + ) { + scopeStack.push(new Map()); + scopePushed = true; + } + + if (ts.isTemplateExpression(node)) { + resolveKeyFromExpression( + node, + scopeStack, + dynamicPrefixes, + baselineNamespaces, + importSpecifiers, + ); + } + + if (ts.isVariableDeclaration(node) && node.initializer) { + const key = resolveKeyFromExpression( + node.initializer, + scopeStack, + dynamicPrefixes, + baselineNamespaces, + importSpecifiers, + ); + if (key && KEY_PATTERN.test(key)) { + if (ts.isIdentifier(node.name)) { + scopeStack[scopeStack.length - 1].set(node.name.text, key); + } + } + } + + if (ts.isCallExpression(node)) { + const classification = classifyCallExpression( + node.expression, + importSpecifiers, + ); + if (classification) { + const [firstArg] = node.arguments; + if (firstArg) { + const key = resolveKeyFromExpression( + firstArg, + scopeStack, + dynamicPrefixes, + baselineNamespaces, + importSpecifiers, + ); + if ( + !addKeyIfValid(key, usedKeys, baselineNamespaces, classification) + ) { + addTemplatePrefixCandidate( + key ?? "", + dynamicPrefixes, + baselineNamespaces, + ); + } + } + } + } + + if (ts.isJsxAttribute(node) && node.name?.text === "i18nKey") { + const initializer = node.initializer; + if (initializer && ts.isStringLiteralLike(initializer)) { + addKeyIfValid(initializer.text, usedKeys, baselineNamespaces, { + forceNamespace: false, + }); + } else if ( + initializer && + ts.isJsxExpression(initializer) && + initializer.expression + ) { + const key = resolveKeyFromExpression( + initializer.expression, + scopeStack, + dynamicPrefixes, + baselineNamespaces, + importSpecifiers, + ); + addKeyIfValid(key, usedKeys, baselineNamespaces, { + forceNamespace: true, + }); + } + } + + node.forEachChild(visit); + + if (scopePushed) { + scopeStack.pop(); + } + }; + + visit(sourceFile); +} + +function collectUsedKeysFromTextFile(file, baselineNamespaces, usedKeys) { + const regex = /['"`]([A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+)+)['"`]/g; + let match; + while ((match = regex.exec(file.content))) { + addKeyIfValid(match[1], usedKeys, baselineNamespaces, { + forceNamespace: false, + }); + } +} + +function collectUsedI18nKeys(sourceFiles, baselineNamespaces) { + const usedKeys = new Set(); + const dynamicPrefixes = new Set(); + + for (const file of sourceFiles) { + if (TS_EXTENSIONS.has(file.extension)) { + collectUsedKeysFromTsFile( + file, + baselineNamespaces, + usedKeys, + dynamicPrefixes, + ); + } else { + collectUsedKeysFromTextFile(file, baselineNamespaces, usedKeys); + } + } + + return { usedKeys, dynamicPrefixes }; +} + function alignToBaseline(baselineNode, localeNode, options) { const shouldCopyLocale = localeNode && typeof localeNode === "object" && !Array.isArray(localeNode); @@ -355,12 +813,54 @@ function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } -function isKeyUsed(content, key) { - if (WHITELIST_KEYS.has(key)) return true; - if (!key) return false; +function findKeyInSources(key, sourceFiles) { + if (fileUsageCache.has(key)) { + return fileUsageCache.get(key); + } const pattern = new RegExp(`(['"\`])${escapeRegExp(key)}\\1`); - return pattern.test(content); + let found = false; + + for (const file of sourceFiles) { + if (pattern.test(file.content)) { + found = true; + break; + } + } + + fileUsageCache.set(key, found); + return found; +} + +function isKeyUsed(key, usage, sourceFiles) { + if (WHITELIST_KEYS.has(key)) return true; + if (!key) return false; + if (usage.usedKeys.has(key)) return true; + + if (dynamicKeyCache.has(key)) { + return dynamicKeyCache.get(key); + } + + const prefixes = getCandidatePrefixes(key); + let used = prefixes.some((prefix) => usage.dynamicPrefixes.has(prefix)); + + if (!used) { + used = findKeyInSources(key, sourceFiles); + } + + dynamicKeyCache.set(key, used); + return used; +} + +function getCandidatePrefixes(key) { + if (!key.includes(".")) return []; + const parts = key.split("."); + const prefixes = []; + for (let index = 0; index < parts.length - 1; index += 1) { + const prefix = `${parts.slice(0, index + 1).join(".")}.`; + prefixes.push(prefix); + } + return prefixes; } function writeReport(reportPath, data) { @@ -403,7 +903,9 @@ function processLocale( locale, baselineData, baselineEntries, - allSourceContent, + usage, + sourceFiles, + missingFromSource, options, ) { const raw = fs.readFileSync(locale.path, "utf8"); @@ -415,15 +917,21 @@ function processLocale( const unused = []; for (const key of flattened.keys()) { - if (!isKeyUsed(allSourceContent, key)) { + if (!isKeyUsed(key, usage, sourceFiles)) { unused.push(key); } } + const sourceMissing = + locale.name === options.baseline + ? missingFromSource.filter((key) => !flattened.has(key)) + : []; + if ( unused.length === 0 && missing.length === 0 && extra.length === 0 && + sourceMissing.length === 0 && !options.align ) { console.log(`[${locale.name}] No issues detected 🎉`); @@ -432,6 +940,10 @@ function processLocale( console.log( ` unused: ${unused.length}, missing vs baseline: ${missing.length}, extra: ${extra.length}`, ); + if (sourceMissing.length > 0) { + console.log(` missing in source: ${sourceMissing.length}`); + logPreviewEntries("missing-source", sourceMissing); + } logPreviewEntries("unused", unused); logPreviewEntries("missing", missing); logPreviewEntries("extra", extra); @@ -479,6 +991,7 @@ function processLocale( removed, missingKeys: missing, extraKeys: extra, + missingSourceKeys: sourceMissing, aligned: aligned && options.apply, }; } @@ -505,7 +1018,7 @@ function main() { console.log(` - ${dir}`); } - const allSourceContent = loadSourceContents(sourceDirs); + const sourceFiles = collectSourceFiles(sourceDirs); const locales = loadLocales(); if (locales.length === 0) { @@ -526,6 +1039,13 @@ function main() { const baselineData = JSON.parse(fs.readFileSync(baselineLocale.path, "utf8")); const baselineEntries = flattenLocale(baselineData); + const baselineNamespaces = new Set(Object.keys(baselineData)); + const usage = collectUsedI18nKeys(sourceFiles, baselineNamespaces); + const baselineKeys = new Set(baselineEntries.keys()); + const missingFromSource = Array.from(usage.usedKeys).filter( + (key) => !baselineKeys.has(key), + ); + missingFromSource.sort(); locales.sort((a, b) => { if (a.name === baselineLocale.name) return -1; @@ -540,7 +1060,9 @@ function main() { locale, baselineData, baselineEntries, - allSourceContent, + usage, + sourceFiles, + missingFromSource, options, ), ); @@ -557,15 +1079,19 @@ function main() { (count, result) => count + result.extraKeys.length, 0, ); + const totalSourceMissing = results.reduce( + (count, result) => count + result.missingSourceKeys.length, + 0, + ); console.log("\nSummary:"); for (const result of results) { console.log( - ` • ${result.locale}: unused=${result.unusedKeys.length}, missing=${result.missingKeys.length}, extra=${result.extraKeys.length}, total=${result.totalKeys}, expected=${result.expectedKeys}`, + ` • ${result.locale}: unused=${result.unusedKeys.length}, missing=${result.missingKeys.length}, extra=${result.extraKeys.length}, missingSource=${result.missingSourceKeys.length}, total=${result.totalKeys}, expected=${result.expectedKeys}`, ); } console.log( - `\nTotals → unused: ${totalUnused}, missing: ${totalMissing}, extra: ${totalExtra}`, + `\nTotals → unused: ${totalUnused}, missing: ${totalMissing}, extra: ${totalExtra}, missingSource: ${totalSourceMissing}`, ); if (options.apply) { console.log( diff --git a/src/components/rule/provider-button.tsx b/src/components/rule/provider-button.tsx index f18aa68b..3a5f027e 100644 --- a/src/components/rule/provider-button.tsx +++ b/src/components/rule/provider-button.tsx @@ -58,11 +58,11 @@ export const ProviderButton = () => { await refreshRules(); await refreshRuleProviders(); - showNotice.success("notices.providers.updateSuccess", { + showNotice.success("providers.notices.updateSuccess", { name, }); } catch (err) { - showNotice.error("notices.providers.updateFailed", { + showNotice.error("providers.notices.updateFailed", { name, message: String(err), }); @@ -78,7 +78,7 @@ export const ProviderButton = () => { // 获取所有provider的名称 const allProviders = Object.keys(ruleProviders || {}); if (allProviders.length === 0) { - showNotice.info("notices.providers.none"); + showNotice.info("providers.notices.none"); return; } @@ -108,9 +108,9 @@ export const ProviderButton = () => { await refreshRules(); await refreshRuleProviders(); - showNotice.success("notices.providers.allUpdated"); + showNotice.success("providers.notices.allUpdated"); } catch (err) { - showNotice.error("notices.providers.genericError", { + showNotice.error("providers.notices.genericError", { message: String(err), }); } finally { diff --git a/src/locales/ar.json b/src/locales/ar.json index 26a32141..f52a297a 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -87,6 +87,11 @@ "lock": "Lock menu order" } }, + "theme": { + "light": "Light", + "dark": "Dark", + "system": "System" + }, "ruleEditor": { "title": "تعديل القواعد", "form": { @@ -106,6 +111,41 @@ "conditionRequired": "شرط القاعدة مطلوب", "invalidRule": "قاعدة غير صالحة" } + }, + "ruleTypes": { + "DOMAIN": "مطابقة اسم المجال الكامل", + "DOMAIN-SUFFIX": "مطابقة لاحقة المجال", + "DOMAIN-KEYWORD": "مطابقة كلمة مفتاحية في المجال", + "DOMAIN-REGEX": "مطابقة المجال باستخدام التعبيرات العادية", + "GEOSITE": "مطابقة المجالات ضمن Geosite", + "GEOIP": "مطابقة رمز البلد لعنوان IP", + "SRC-GEOIP": "مطابقة رمز البلد لعنوان IP المصدر", + "IP-ASN": "مطابقة ASN لعنوان IP", + "SRC-IP-ASN": "مطابقة ASN لعنوان IP المصدر", + "IP-CIDR": "مطابقة نطاق عنوان IP", + "IP-CIDR6": "مطابقة نطاق عناوين IPv6", + "SRC-IP-CIDR": "مطابقة نطاق عنوان IP المصدر", + "IP-SUFFIX": "مطابقة لاحقة عنوان IP", + "SRC-IP-SUFFIX": "مطابقة لاحقة عنوان IP المصدر", + "SRC-PORT": "مطابقة نطاق المنفذ المصدر", + "DST-PORT": "مطابقة نطاق المنفذ الوجهة", + "IN-PORT": "مطابقة المنفذ الوارد", + "DSCP": "علامة DSCP (لـ tproxy على UDP فقط)", + "PROCESS-NAME": "مطابقة اسم العملية (اسم حزمة Android)", + "PROCESS-PATH": "مطابقة المسار الكامل للعملية", + "PROCESS-NAME-REGEX": "مطابقة اسم العملية باستخدام التعبيرات العادية (اسم حزمة Android)", + "PROCESS-PATH-REGEX": "مطابقة المسار الكامل للعملية باستخدام التعبيرات العادية", + "NETWORK": "مطابقة بروتوكول النقل (TCP/UDP)", + "UID": "مطابقة معرف المستخدم في Linux", + "IN-TYPE": "مطابقة نوع الإدخال", + "IN-USER": "مطابقة اسم المستخدم للإدخال", + "IN-NAME": "مطابقة اسم الإدخال", + "SUB-RULE": "قاعدة فرعية", + "RULE-SET": "مطابقة مجموعة القواعد", + "AND": "منطقي AND", + "OR": "منطقي OR", + "NOT": "منطقي NOT", + "MATCH": "مطابقة جميع الطلبات" } }, "profile": { @@ -1009,6 +1049,11 @@ "default": "مجموعات الوكلاء", "chainMode": "Proxy Chain Mode" }, + "modes": { + "rule": "Rule", + "global": "Global", + "direct": "Direct" + }, "actions": { "toggleChain": "🔗 بروكسي السلسلة", "connect": "Connect", @@ -1184,6 +1229,12 @@ "fallback": "التبديل إلى وكيل آخر عند حدوث خطأ", "load-balance": "توزيع التحميل بين الوكلاء", "relay": "التمرير عبر سلسلة الوكلاء المحددة" + }, + "policies": { + "DIRECT": "البيانات تخرج مباشرة", + "REJECT": "رفض الطلبات", + "REJECT-DROP": "تجاهل الطلبات", + "PASS": "تخطي هذه القاعدة عند المطابقة" } }, "system": { diff --git a/src/locales/de.json b/src/locales/de.json index 466cf025..1e39898a 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -87,6 +87,11 @@ "lock": "Lock menu order" } }, + "theme": { + "light": "Light", + "dark": "Dark", + "system": "System" + }, "ruleEditor": { "title": "Regeln bearbeiten", "form": { @@ -106,6 +111,41 @@ "conditionRequired": "Regelbedingung fehlt", "invalidRule": "Ungültige Regel" } + }, + "ruleTypes": { + "DOMAIN": "Vollständigen Domainnamen übereinstimmen", + "DOMAIN-SUFFIX": "Domain-Suffix übereinstimmen", + "DOMAIN-KEYWORD": "Domain-Schlüsselwort übereinstimmen", + "DOMAIN-REGEX": "Domain-Regulärer Ausdruck übereinstimmen", + "GEOSITE": "Domainnamen in Geosite übereinstimmen", + "GEOIP": "IP-Ländercode übereinstimmen", + "SRC-GEOIP": "Quell-IP-Ländercode übereinstimmen", + "IP-ASN": "IP-ASN übereinstimmen", + "SRC-IP-ASN": "Quell-IP-ASN übereinstimmen", + "IP-CIDR": "IP-Adressbereich übereinstimmen", + "IP-CIDR6": "IP-Adressbereich übereinstimmen", + "SRC-IP-CIDR": "Quell-IP-Adressbereich übereinstimmen", + "IP-SUFFIX": "IP-Suffix-Bereich übereinstimmen", + "SRC-IP-SUFFIX": "Quell-IP-Suffix-Bereich übereinstimmen", + "SRC-PORT": "Quellportbereich der Anfrage übereinstimmen", + "DST-PORT": "Zielportbereich der Anfrage übereinstimmen", + "IN-PORT": "Eingangsport übereinstimmen", + "DSCP": "DSCP-Markierung (nur für TPROXY UDP-Eingang)", + "PROCESS-NAME": "Prozessnamen übereinstimmen (Android-Paketname)", + "PROCESS-PATH": "Vollständigen Prozesspfad übereinstimmen", + "PROCESS-NAME-REGEX": "Regulärer Ausdruck für vollständigen Prozessnamen übereinstimmen (Android-Paketname)", + "PROCESS-PATH-REGEX": "Regulärer Ausdruck für vollständigen Prozesspfad übereinstimmen", + "NETWORK": "Übertragungsprotokoll übereinstimmen (TCP/UDP)", + "UID": "Linux-USER-ID übereinstimmen", + "IN-TYPE": "Eingangstyp übereinstimmen", + "IN-USER": "Eingangsbenutzername übereinstimmen", + "IN-NAME": "Eingangsname übereinstimmen", + "SUB-RULE": "Unterregel", + "RULE-SET": "Regelsatz übereinstimmen", + "AND": "Logisches UND", + "OR": "Logisches ODER", + "NOT": "Logisches NICHT", + "MATCH": "Alle Anfragen übereinstimmen" } }, "profile": { @@ -1009,6 +1049,11 @@ "default": "Proxy-Gruppen", "chainMode": "Proxy Chain Mode" }, + "modes": { + "rule": "Rule", + "global": "Global", + "direct": "Direct" + }, "actions": { "toggleChain": "🔗 Ketten-Proxy", "connect": "Connect", @@ -1184,6 +1229,12 @@ "fallback": "Bei Nichtverfügbarkeit zu einem anderen Proxy wechseln", "load-balance": "Proxy basierend auf Lastverteilung zuweisen", "relay": "Basierend auf definiertem Proxy-Kette weiterleiten" + }, + "policies": { + "DIRECT": "Direktverbindung", + "REJECT": "Anfrage ablehnen", + "REJECT-DROP": "Anfrage verwerfen", + "PASS": "Diese Regel überspringen" } }, "system": { diff --git a/src/locales/en.json b/src/locales/en.json index 6ac4a616..f195647d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -87,6 +87,11 @@ "lock": "Lock menu order" } }, + "theme": { + "light": "Light", + "dark": "Dark", + "system": "System" + }, "ruleEditor": { "title": "Edit Rules", "form": { @@ -106,6 +111,41 @@ "conditionRequired": "Rule Condition Required", "invalidRule": "Invalid Rule" } + }, + "ruleTypes": { + "DOMAIN": "Matches the full domain name", + "DOMAIN-SUFFIX": "Matches the domain suffix", + "DOMAIN-KEYWORD": "Matches the domain keyword", + "DOMAIN-REGEX": "Matches the domain using regular expressions", + "GEOSITE": "Matches domains within the Geosite", + "GEOIP": "Matches the country code of the IP address", + "SRC-GEOIP": "Matches the country code of the source IP address", + "IP-ASN": "Matches the IP address's ASN", + "SRC-IP-ASN": "Matches the source IP address's ASN", + "IP-CIDR": "Matches the IP address range", + "IP-CIDR6": "Matches the IPv6 address range", + "SRC-IP-CIDR": "Matches the source IP address range", + "IP-SUFFIX": "Matches the IP address suffix range", + "SRC-IP-SUFFIX": "Matches the source IP address suffix range", + "SRC-PORT": "Matches the source port range", + "DST-PORT": "Matches the destination port range", + "IN-PORT": "Matches the inbound port", + "DSCP": "DSCP marking (only for tproxy UDP inbound)", + "PROCESS-NAME": "Matches the process name (Android package name)", + "PROCESS-PATH": "Matches the full process path", + "PROCESS-NAME-REGEX": "Matches the full process name using regular expressions (Android package name)", + "PROCESS-PATH-REGEX": "Matches the full process path using regular expressions", + "NETWORK": "Matches the transport protocol (tcp/udp)", + "UID": "Matches the Linux USER ID", + "IN-TYPE": "Matches the inbound type", + "IN-USER": "Matches the inbound username", + "IN-NAME": "Matches the inbound name", + "SUB-RULE": "Sub-rule", + "RULE-SET": "Matches the rule set", + "AND": "Logical AND", + "OR": "Logical OR", + "NOT": "Logical NOT", + "MATCH": "Matches all requests" } }, "profile": { @@ -1009,6 +1049,11 @@ "default": "Proxy Groups", "chainMode": "Proxy Chain Mode" }, + "modes": { + "rule": "Rule", + "global": "Global", + "direct": "Direct" + }, "actions": { "toggleChain": "🔗 Chain Proxy", "connect": "Connect", @@ -1184,6 +1229,12 @@ "fallback": "Switch to another proxy on error", "load-balance": "Distribute proxy based on load balancing", "relay": "Pass through the defined proxy chain" + }, + "policies": { + "DIRECT": "Data goes directly outbound", + "REJECT": "Intercepts requests", + "REJECT-DROP": "Discards requests", + "PASS": "Skips this rule when matched" } }, "system": { diff --git a/src/locales/es.json b/src/locales/es.json index f614c50a..eb52ba25 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -87,6 +87,11 @@ "lock": "Lock menu order" } }, + "theme": { + "light": "Light", + "dark": "Dark", + "system": "System" + }, "ruleEditor": { "title": "Editar reglas", "form": { @@ -106,6 +111,41 @@ "conditionRequired": "Falta la condición de la regla", "invalidRule": "Regla no válida" } + }, + "ruleTypes": { + "DOMAIN": "Coincidir con el nombre de dominio completo", + "DOMAIN-SUFFIX": "Coincidir con el sufijo del nombre de dominio", + "DOMAIN-KEYWORD": "Coincidir con la palabra clave del nombre de dominio", + "DOMAIN-REGEX": "Coincidir con la expresión regular del nombre de dominio", + "GEOSITE": "Coincidir con los nombres de dominio en Geosite", + "GEOIP": "Coincidir con el código de país del IP", + "SRC-GEOIP": "Coincidir con el código de país del IP de origen", + "IP-ASN": "Coincidir con el ASN del IP", + "SRC-IP-ASN": "Coincidir con el ASN del IP de origen", + "IP-CIDR": "Coincidir con el rango de direcciones IP", + "IP-CIDR6": "Coincidir con el rango de direcciones IP", + "SRC-IP-CIDR": "Coincidir con el rango de direcciones IP de origen", + "IP-SUFFIX": "Coincidir con el rango de sufijos de IP", + "SRC-IP-SUFFIX": "Coincidir con el rango de sufijos de IP de origen", + "SRC-PORT": "Coincidir con el rango de puertos de origen de la solicitud", + "DST-PORT": "Coincidir con el rango de puertos de destino de la solicitud", + "IN-PORT": "Coincidir con el puerto de entrada", + "DSCP": "Etiqueta DSCP (solo para entradas UDP TPROXY)", + "PROCESS-NAME": "Coincidir con el nombre del proceso (nombre del paquete de Android)", + "PROCESS-PATH": "Coincidir con la ruta completa del proceso", + "PROCESS-NAME-REGEX": "Coincidir con el nombre completo del proceso mediante expresiones regulares (nombre del paquete de Android)", + "PROCESS-PATH-REGEX": "Coincidir con la ruta completa del proceso mediante expresiones regulares", + "NETWORK": "Coincidir con el protocolo de transporte (TCP/UDP)", + "UID": "Coincidir con el ID de usuario de Linux", + "IN-TYPE": "Coincidir con el tipo de entrada", + "IN-USER": "Coincidir con el nombre de usuario de entrada", + "IN-NAME": "Coincidir con el nombre de entrada", + "SUB-RULE": "Subregla", + "RULE-SET": "Coincidir con el conjunto de reglas", + "AND": "Y lógico", + "OR": "O lógico", + "NOT": "No lógico", + "MATCH": "Coincidir con todas las solicitudes" } }, "profile": { @@ -1009,6 +1049,11 @@ "default": "Grupos de proxies", "chainMode": "Proxy Chain Mode" }, + "modes": { + "rule": "Rule", + "global": "Global", + "direct": "Direct" + }, "actions": { "toggleChain": "🔗 Proxy en cadena", "connect": "Connect", @@ -1184,6 +1229,12 @@ "fallback": "Cambiar a otro proxy cuando no esté disponible", "load-balance": "Asignar proxy según el equilibrio de carga", "relay": "Transferir según la cadena de proxy definida" + }, + "policies": { + "DIRECT": "Conexión directa", + "REJECT": "Rechazar solicitud", + "REJECT-DROP": "Descartar solicitud", + "PASS": "Saltar esta regla" } }, "system": { diff --git a/src/locales/fa.json b/src/locales/fa.json index 849b489d..6445fade 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -87,6 +87,11 @@ "lock": "Lock menu order" } }, + "theme": { + "light": "Light", + "dark": "Dark", + "system": "System" + }, "ruleEditor": { "title": "ویرایش قوانین", "form": { @@ -106,6 +111,41 @@ "conditionRequired": "شرط قانون الزامی است", "invalidRule": "قانون نامعتبر" } + }, + "ruleTypes": { + "DOMAIN": "مطابقت با نام کامل دامنه", + "DOMAIN-SUFFIX": "مطابقت با پسوند دامنه", + "DOMAIN-KEYWORD": "مطابقت با کلمه کلیدی دامنه", + "DOMAIN-REGEX": "مطابقت با دامنه با استفاده از عبارات منظم", + "GEOSITE": "مطابقت با دامنه‌های درون Geosite", + "GEOIP": "مطابقت با کد کشور IP", + "SRC-GEOIP": "مطابقت با کد کشور IP مبدا", + "IP-ASN": "مطابقت با ASN آدرس IP", + "SRC-IP-ASN": "مطابقت با ASN آدرس IP مبدا", + "IP-CIDR": "مطابقت با محدوده آدرس IP", + "IP-CIDR6": "مطابقت با محدوده آدرس IPv6", + "SRC-IP-CIDR": "مطابقت با محدوده آدرس IP مبدا", + "IP-SUFFIX": "مطابقت با محدوده پسوند آدرس IP", + "SRC-IP-SUFFIX": "مطابقت با محدوده پسوند آدرس IP مبدا", + "SRC-PORT": "مطابقت با محدوده پورت مبدا", + "DST-PORT": "مطابقت با محدوده پورت مقصد", + "IN-PORT": "مطابقت با پورت ورودی", + "DSCP": "علامت‌گذاری DSCP (فقط برای tproxy UDP ورودی)", + "PROCESS-NAME": "مطابقت با نام فرآیند (نام بسته Android)", + "PROCESS-PATH": "مطابقت با مسیر کامل فرآیند", + "PROCESS-NAME-REGEX": "مطابقت با نام فرآیند با استفاده از عبارات منظم (نام بسته Android)", + "PROCESS-PATH-REGEX": "مطابقت با مسیر کامل فرآیند با استفاده از عبارات منظم", + "NETWORK": "مطابقت با پروتکل انتقال (tcp/udp)", + "UID": "مطابقت با شناسه کاربری Linux", + "IN-TYPE": "مطابقت با نوع ورودی", + "IN-USER": "مطابقت با نام کاربری ورودی", + "IN-NAME": "مطابقت با نام ورودی", + "SUB-RULE": "قانون فرعی", + "RULE-SET": "مطابقت با مجموعه قوانین", + "AND": "منطق AND", + "OR": "منطق OR", + "NOT": "منطق NOT", + "MATCH": "مطابقت با تمام درخواست‌ها" } }, "profile": { @@ -1009,6 +1049,11 @@ "default": "گروه‌های پراکسی", "chainMode": "Proxy Chain Mode" }, + "modes": { + "rule": "Rule", + "global": "Global", + "direct": "Direct" + }, "actions": { "toggleChain": "🔗 پراکسی زنجیره‌ای", "connect": "Connect", @@ -1184,6 +1229,12 @@ "fallback": "تعویض به پروکسی دیگر در صورت بروز خطا", "load-balance": "توزیع پراکسی بر اساس توازن بار", "relay": "عبور از زنجیره پروکسی تعریف شده" + }, + "policies": { + "DIRECT": "داده‌ها به صورت مستقیم خروجی می‌شوند", + "REJECT": "درخواست‌ها را متوقف می‌کند", + "REJECT-DROP": "درخواست‌ها را نادیده می‌گیرد", + "PASS": "این قانون را در صورت تطابق نادیده می‌گیرد" } }, "system": { diff --git a/src/locales/id.json b/src/locales/id.json index e43138ac..b403f548 100644 --- a/src/locales/id.json +++ b/src/locales/id.json @@ -87,6 +87,11 @@ "lock": "Lock menu order" } }, + "theme": { + "light": "Light", + "dark": "Dark", + "system": "System" + }, "ruleEditor": { "title": "Ubah Aturan", "form": { @@ -106,6 +111,41 @@ "conditionRequired": "Kondisi Aturan Diperlukan", "invalidRule": "Aturan Tidak Valid" } + }, + "ruleTypes": { + "DOMAIN": "Cocok dengan nama domain lengkap", + "DOMAIN-SUFFIX": "Cocok dengan sufiks domain", + "DOMAIN-KEYWORD": "Cocok dengan kata kunci domain", + "DOMAIN-REGEX": "Cocok dengan domain menggunakan ekspresi reguler", + "GEOSITE": "Cocok dengan domain dalam Geosite", + "GEOIP": "Cocok dengan kode negara alamat IP", + "SRC-GEOIP": "Cocok dengan kode negara alamat IP sumber", + "IP-ASN": "Cocok dengan ASN alamat IP", + "SRC-IP-ASN": "Cocok dengan ASN alamat IP sumber", + "IP-CIDR": "Cocok dengan rentang alamat IP", + "IP-CIDR6": "Cocok dengan rentang alamat IPv6", + "SRC-IP-CIDR": "Cocok dengan rentang alamat IP sumber", + "IP-SUFFIX": "Cocok dengan rentang sufiks alamat IP", + "SRC-IP-SUFFIX": "Cocok dengan rentang sufiks alamat IP sumber", + "SRC-PORT": "Cocok dengan rentang port sumber", + "DST-PORT": "Cocok dengan rentang port tujuan", + "IN-PORT": "Cocok dengan port masuk", + "DSCP": "Penandaan DSCP (hanya untuk tproxy UDP masuk)", + "PROCESS-NAME": "Cocok dengan nama proses (nama paket Android)", + "PROCESS-PATH": "Cocok dengan jalur proses lengkap", + "PROCESS-NAME-REGEX": "Cocok dengan nama proses lengkap menggunakan ekspresi reguler (nama paket Android)", + "PROCESS-PATH-REGEX": "Cocok dengan jalur proses lengkap menggunakan ekspresi reguler", + "NETWORK": "Cocok dengan protokol transportasi (tcp/udp)", + "UID": "Cocok dengan ID PENGGUNA Linux", + "IN-TYPE": "Cocok dengan jenis masuk", + "IN-USER": "Cocok dengan nama pengguna masuk", + "IN-NAME": "Cocok dengan nama masuk", + "SUB-RULE": "Sub-aturan", + "RULE-SET": "Cocok dengan set aturan", + "AND": "Logika DAN", + "OR": "Logika ATAU", + "NOT": "Logika TIDAK", + "MATCH": "Cocok dengan semua permintaan" } }, "profile": { @@ -1009,6 +1049,11 @@ "default": "Grup Proksi", "chainMode": "Proxy Chain Mode" }, + "modes": { + "rule": "Rule", + "global": "Global", + "direct": "Direct" + }, "actions": { "toggleChain": "🔗 Proxy Rantai", "connect": "Connect", @@ -1184,6 +1229,12 @@ "fallback": "Beralih ke proksi lain saat terjadi kesalahan", "load-balance": "Distribusikan proksi berdasarkan penyeimbangan beban", "relay": "Lewatkan melalui rantai proksi yang ditentukan" + }, + "policies": { + "DIRECT": "Data langsung keluar", + "REJECT": "Mencegat permintaan", + "REJECT-DROP": "Membuang permintaan", + "PASS": "Lewati aturan ini saat cocok" } }, "system": { diff --git a/src/locales/jp.json b/src/locales/jp.json index be974cf4..c10f1b3e 100644 --- a/src/locales/jp.json +++ b/src/locales/jp.json @@ -87,6 +87,11 @@ "lock": "Lock menu order" } }, + "theme": { + "light": "Light", + "dark": "Dark", + "system": "System" + }, "ruleEditor": { "title": "ルールを編集", "form": { @@ -106,6 +111,41 @@ "conditionRequired": "ルール条件が必要です", "invalidRule": "無効なルール" } + }, + "ruleTypes": { + "DOMAIN": "完全なドメイン名を一致させる", + "DOMAIN-SUFFIX": "ドメインサフィックスを一致させる", + "DOMAIN-KEYWORD": "ドメインキーワードを一致させる", + "DOMAIN-REGEX": "ドメイン正規表現を一致させる", + "GEOSITE": "Geosite内のドメインを一致させる", + "GEOIP": "IPの所属国コードを一致させる", + "SRC-GEOIP": "送信元IPの所属国コードを一致させる", + "IP-ASN": "IPの所属ASNを一致させる", + "SRC-IP-ASN": "送信元IPの所属ASNを一致させる", + "IP-CIDR": "IPアドレス範囲を一致させる", + "IP-CIDR6": "IPアドレス範囲を一致させる", + "SRC-IP-CIDR": "送信元IPアドレス範囲を一致させる", + "IP-SUFFIX": "IPサフィックス範囲を一致させる", + "SRC-IP-SUFFIX": "送信元IPサフィックス範囲を一致させる", + "SRC-PORT": "送信元ポート範囲を一致させる", + "DST-PORT": "宛先ポート範囲を一致させる", + "IN-PORT": "入力ポートを一致させる", + "DSCP": "DSCPマーク(TPROXY UDP入力のみ)", + "PROCESS-NAME": "プロセス名を一致させる(Androidパッケージ名)", + "PROCESS-PATH": "完全なプロセスパスを一致させる", + "PROCESS-NAME-REGEX": "完全なプロセス名を正規表現で一致させる(Androidパッケージ名)", + "PROCESS-PATH-REGEX": "完全なプロセスパスを正規表現で一致させる", + "NETWORK": "トランスポートプロトコルを一致させる (TCP/UDP)", + "UID": "LinuxユーザーIDを一致させる", + "IN-TYPE": "入力タイプを一致させる", + "IN-USER": "入力ユーザー名を一致させる", + "IN-NAME": "入力名を一致させる", + "SUB-RULE": "サブルール", + "RULE-SET": "ルールセットを一致させる", + "AND": "論理積", + "OR": "論理和", + "NOT": "論理否定", + "MATCH": "すべてのリクエストを一致させる" } }, "profile": { @@ -1009,6 +1049,11 @@ "default": "プロキシグループ", "chainMode": "Proxy Chain Mode" }, + "modes": { + "rule": "Rule", + "global": "Global", + "direct": "Direct" + }, "actions": { "toggleChain": "🔗 チェーンプロキシ", "connect": "Connect", @@ -1184,6 +1229,12 @@ "fallback": "利用不可の場合は別のプロキシに切り替える", "load-balance": "負荷分散によりプロキシを割り当てる", "relay": "定義されたプロキシチェーンに沿って転送する" + }, + "policies": { + "DIRECT": "直接接続", + "REJECT": "リクエストを拒否", + "REJECT-DROP": "リクエストを破棄", + "PASS": "このルールをスキップ" } }, "system": { diff --git a/src/locales/ko.json b/src/locales/ko.json index 285ff4f8..841c408b 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -87,6 +87,11 @@ "lock": "Lock menu order" } }, + "theme": { + "light": "Light", + "dark": "Dark", + "system": "System" + }, "ruleEditor": { "title": "규칙 편집", "form": { @@ -106,6 +111,41 @@ "conditionRequired": "규칙 조건 필요", "invalidRule": "잘못된 규칙" } + }, + "ruleTypes": { + "DOMAIN": "전체 도메인 이름과 일치", + "DOMAIN-SUFFIX": "도메인 접미사와 일치", + "DOMAIN-KEYWORD": "도메인 키워드와 일치", + "DOMAIN-REGEX": "정규 표현식을 사용한 도메인 일치", + "GEOSITE": "Geosite 내의 도메인과 일치", + "GEOIP": "IP 주소의 국가 코드와 일치", + "SRC-GEOIP": "소스 IP 주소의 국가 코드와 일치", + "IP-ASN": "IP 주소의 ASN과 일치", + "SRC-IP-ASN": "소스 IP 주소의 ASN과 일치", + "IP-CIDR": "IP 주소 범위와 일치", + "IP-CIDR6": "IPv6 주소 범위와 일치", + "SRC-IP-CIDR": "소스 IP 주소 범위와 일치", + "IP-SUFFIX": "IP 주소 접미사 범위와 일치", + "SRC-IP-SUFFIX": "소스 IP 주소 접미사 범위와 일치", + "SRC-PORT": "소스 포트 범위와 일치", + "DST-PORT": "대상 포트 범위와 일치", + "IN-PORT": "인바운드 포트와 일치", + "DSCP": "DSCP 마킹(tproxy UDP 인바운드만 해당)", + "PROCESS-NAME": "프로세스 이름과 일치(안드로이드 패키지 이름)", + "PROCESS-PATH": "전체 프로세스 경로와 일치", + "PROCESS-NAME-REGEX": "정규 표현식을 사용한 전체 프로세스 이름 일치(안드로이드 패키지 이름)", + "PROCESS-PATH-REGEX": "정규 표현식을 사용한 전체 프로세스 경로 일치", + "NETWORK": "전송 프로토콜과 일치(tcp/udp)", + "UID": "Linux 사용자 ID와 일치", + "IN-TYPE": "인바운드 유형과 일치", + "IN-USER": "인바운드 사용자 이름과 일치", + "IN-NAME": "인바운드 이름과 일치", + "SUB-RULE": "하위 규칙", + "RULE-SET": "규칙 세트와 일치", + "AND": "논리 AND", + "OR": "논리 OR", + "NOT": "논리 NOT", + "MATCH": "모든 요청과 일치" } }, "profile": { @@ -1009,6 +1049,11 @@ "default": "프록시 그룹", "chainMode": "Proxy Chain Mode" }, + "modes": { + "rule": "Rule", + "global": "Global", + "direct": "Direct" + }, "actions": { "toggleChain": "🔗 체인 프록시", "connect": "Connect", @@ -1184,6 +1229,12 @@ "fallback": "오류 발생 시 다른 프록시로 전환", "load-balance": "부하 분산에 따라 프록시 분배", "relay": "정의된 프록시 체인을 통과" + }, + "policies": { + "DIRECT": "데이터가 직접 아웃바운드로 이동", + "REJECT": "요청 차단", + "REJECT-DROP": "요청 폐기", + "PASS": "일치할 경우 이 규칙 건너뛰기" } }, "system": { diff --git a/src/locales/ru.json b/src/locales/ru.json index 165ead80..b7478c4f 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -87,6 +87,11 @@ "lock": "Lock menu order" } }, + "theme": { + "light": "Light", + "dark": "Dark", + "system": "System" + }, "ruleEditor": { "title": "Редактировать правила", "form": { @@ -106,6 +111,41 @@ "conditionRequired": "Требуется условие правила", "invalidRule": "Недействительное правило" } + }, + "ruleTypes": { + "DOMAIN": "Соответствует полному доменному имени", + "DOMAIN-SUFFIX": "Соответствует суффиксу домена", + "DOMAIN-KEYWORD": "Соответствует ключевому слову домена", + "DOMAIN-REGEX": "Соответствует домену с использованием регулярных выражений", + "GEOSITE": "Соответствует доменам в Geosite", + "GEOIP": "Соответствует коду страны IP-адреса", + "SRC-GEOIP": "Соответствует коду страны исходного IP-адреса", + "IP-ASN": "Соответствует ASN IP-адреса", + "SRC-IP-ASN": "Соответствует ASN исходного IP-адреса", + "IP-CIDR": "Соответствует диапазону IP-адресов", + "IP-CIDR6": "Соответствует диапазону IPv6-адресов", + "SRC-IP-CIDR": "Соответствует диапазону исходных IP-адресов", + "IP-SUFFIX": "Соответствует диапазону суффиксов IP-адресов", + "SRC-IP-SUFFIX": "Соответствует диапазону суффиксов исходных IP-адресов", + "SRC-PORT": "Соответствует диапазону исходных портов", + "DST-PORT": "Соответствует диапазону целевых портов", + "IN-PORT": "Соответствует входящему порту", + "DSCP": "Маркировка DSCP (только для tproxy UDP входящего)", + "PROCESS-NAME": "Соответствует имени процесса (имя пакета Android)", + "PROCESS-PATH": "Соответствует полному пути процесса", + "PROCESS-NAME-REGEX": "Соответствует имени процесса с использованием регулярных выражений (имя пакета Android)", + "PROCESS-PATH-REGEX": "Соответствует полному пути процесса с использованием регулярных выражений", + "NETWORK": "Соответствует транспортному протоколу (tcp/udp)", + "UID": "Соответствует USER ID в Linux", + "IN-TYPE": "Соответствует типу входящего соединения", + "IN-USER": "Соответствует имени пользователя входящего соединения", + "IN-NAME": "Соответствует имени входящего соединения", + "SUB-RULE": "Подправило", + "RULE-SET": "Соответствует набору правил", + "AND": "Логическое И", + "OR": "Логическое ИЛИ", + "NOT": "Логическое НЕ", + "MATCH": "Соответствует всем запросам" } }, "profile": { @@ -1009,6 +1049,11 @@ "default": "Группы прокси", "chainMode": "Proxy Chain Mode" }, + "modes": { + "rule": "Rule", + "global": "Global", + "direct": "Direct" + }, "actions": { "toggleChain": "🔗 Цепной прокси", "connect": "Connect", @@ -1184,6 +1229,12 @@ "fallback": "Переключение на другой прокси при ошибке", "load-balance": "Распределение прокси на основе балансировки нагрузки", "relay": "Передача через определенную цепочку прокси" + }, + "policies": { + "DIRECT": "Данные направляются напрямую наружу", + "REJECT": "Перехватывает запросы", + "REJECT-DROP": "Отклоняет запросы", + "PASS": "Пропускает это правило при совпадении" } }, "system": { diff --git a/src/locales/tr.json b/src/locales/tr.json index 061e30fd..932da83a 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -87,6 +87,11 @@ "lock": "Lock menu order" } }, + "theme": { + "light": "Light", + "dark": "Dark", + "system": "System" + }, "ruleEditor": { "title": "Kuralları Düzenle", "form": { @@ -106,6 +111,41 @@ "conditionRequired": "Kural Koşulu Gerekli", "invalidRule": "Geçersiz Kural" } + }, + "ruleTypes": { + "DOMAIN": "Tam alan adıyla eşleşir", + "DOMAIN-SUFFIX": "Alan adı sonekiyle eşleşir", + "DOMAIN-KEYWORD": "Alan adı anahtar kelimesiyle eşleşir", + "DOMAIN-REGEX": "Alan adını düzenli ifadeler kullanarak eşleştirir", + "GEOSITE": "Geosite içindeki alan adlarıyla eşleşir", + "GEOIP": "IP adresinin ülke koduyla eşleşir", + "SRC-GEOIP": "Kaynak IP adresinin ülke koduyla eşleşir", + "IP-ASN": "IP adresinin ASN'siyle eşleşir", + "SRC-IP-ASN": "Kaynak IP adresinin ASN'siyle eşleşir", + "IP-CIDR": "IP adresi aralığıyla eşleşir", + "IP-CIDR6": "IPv6 adresi aralığıyla eşleşir", + "SRC-IP-CIDR": "Kaynak IP adresi aralığıyla eşleşir", + "IP-SUFFIX": "IP adresi sonek aralığıyla eşleşir", + "SRC-IP-SUFFIX": "Kaynak IP adresi sonek aralığıyla eşleşir", + "SRC-PORT": "Kaynak port aralığıyla eşleşir", + "DST-PORT": "Hedef port aralığıyla eşleşir", + "IN-PORT": "Gelen port ile eşleşir", + "DSCP": "DSCP işaretlemesi (sadece tvekil UDP girişi için)", + "PROCESS-NAME": "İşlem adıyla eşleşir (Android paket adı)", + "PROCESS-PATH": "Tam işlem yoluyla eşleşir", + "PROCESS-NAME-REGEX": "Tam işlem adını düzenli ifadeler kullanarak eşleştirir (Android paket adı)", + "PROCESS-PATH-REGEX": "Tam işlem yolunu düzenli ifadeler kullanarak eşleştirir", + "NETWORK": "Taşıma protokolüyle eşleşir (tcp/udp)", + "UID": "Linux KULLANICI ID'siyle eşleşir", + "IN-TYPE": "Gelen bağlantı tipiyle eşleşir", + "IN-USER": "Gelen bağlantı kullanıcı adıyla eşleşir", + "IN-NAME": "Gelen bağlantı adıyla eşleşir", + "SUB-RULE": "Alt kural", + "RULE-SET": "Kural setiyle eşleşir", + "AND": "Mantıksal VE", + "OR": "Mantıksal VEYA", + "NOT": "Mantıksal DEĞİL", + "MATCH": "Tüm isteklerle eşleşir" } }, "profile": { @@ -1009,6 +1049,11 @@ "default": "Vekil Grupları", "chainMode": "Proxy Chain Mode" }, + "modes": { + "rule": "Rule", + "global": "Global", + "direct": "Direct" + }, "actions": { "toggleChain": "🔗 Zincir Proxy", "connect": "Connect", @@ -1184,6 +1229,12 @@ "fallback": "Hata durumunda başka bir vekil'e geçin", "load-balance": "Yük dengelemeye göre vekil dağıtın", "relay": "Tanımlanan vekil zincirinden geçirin" + }, + "policies": { + "DIRECT": "Veri doğrudan dışarı gider", + "REJECT": "İstekleri engeller", + "REJECT-DROP": "İstekleri atar", + "PASS": "Eşleştiğinde bu kuralı atlar" } }, "system": { diff --git a/src/locales/tt.json b/src/locales/tt.json index 68d653a8..b1215f1e 100644 --- a/src/locales/tt.json +++ b/src/locales/tt.json @@ -87,6 +87,11 @@ "lock": "Lock menu order" } }, + "theme": { + "light": "Light", + "dark": "Dark", + "system": "System" + }, "ruleEditor": { "title": "Кагыйдәләрне үзгәртү", "form": { @@ -106,6 +111,41 @@ "conditionRequired": "Кагыйдә шарты кирәк", "invalidRule": "Яраксыз кагыйдә" } + }, + "ruleTypes": { + "DOMAIN": "Домен исеменең тулы туры килүе", + "DOMAIN-SUFFIX": "Домен суффиксына туры килү", + "DOMAIN-KEYWORD": "Доменда төп сүзгә туры килү", + "DOMAIN-REGEX": "Доменны регекс аша туры китерү", + "GEOSITE": "Geosite исемлегендәге доменга туры килү", + "GEOIP": "IP-адресның ил коды буенча туры килү", + "SRC-GEOIP": "Чыганак IP-адресның ил коды буенча туры килү", + "IP-ASN": "IP-адрес ASN'ы буенча туры килү", + "SRC-IP-ASN": "Чыганак IP-адрес ASN'ы буенча туры килү", + "IP-CIDR": "IP-адреслар диапазонына туры килү", + "IP-CIDR6": "IPv6 адреслар диапазонына туры килү", + "SRC-IP-CIDR": "Чыганак IP-адреслар диапазонына туры килү", + "IP-SUFFIX": "IP-адрес суффиксына туры килү", + "SRC-IP-SUFFIX": "Чыганак IP-адрес суффиксына туры килү", + "SRC-PORT": "Чыганак портлар диапазонына туры килү", + "DST-PORT": "Максат портлар диапазонына туры килү", + "IN-PORT": "Керүче портка туры килү", + "DSCP": "DSCP тамгалавы (tproxy UDP өчен)", + "PROCESS-NAME": "Процесс исеменә туры килү (Android пакет исеме)", + "PROCESS-PATH": "Процесс юлына туры килү", + "PROCESS-NAME-REGEX": "Процесс исемен регекс белән туры китерү (Android пакет исеме)", + "PROCESS-PATH-REGEX": "Процесс юлын регекс белән туры китерү", + "NETWORK": "Транспорт протоколына (tcp/udp) туры килү", + "UID": "Linux USER ID'га туры килү", + "IN-TYPE": "Керүче тоташу төренә туры килү", + "IN-USER": "Керүче тоташу кулланучысына туры килү", + "IN-NAME": "Керүче тоташу исеменә туры килү", + "SUB-RULE": "Кушымча кагыйдә", + "RULE-SET": "Кагыйдәләр тупланмасына туры килү", + "AND": "Логик ҺӘМ", + "OR": "Логик ЯКИ", + "NOT": "Логик ТҮГЕЛ", + "MATCH": "Барлык сорауларга туры килә" } }, "profile": { @@ -1009,6 +1049,11 @@ "default": "Прокси төркемнәре", "chainMode": "Proxy Chain Mode" }, + "modes": { + "rule": "Rule", + "global": "Global", + "direct": "Direct" + }, "actions": { "toggleChain": "🔗 Чылбыр прокси", "connect": "Connect", @@ -1184,6 +1229,12 @@ "fallback": "Хата булган очракта башка проксига күчү", "load-balance": "Трафикны баланслау нигезендә прокси тарату", "relay": "Билгеле прокси чылбыры аша тапшыру" + }, + "policies": { + "DIRECT": "Туры чыгу", + "REJECT": "Сорауларны тоткарлау", + "REJECT-DROP": "Сорауларны кире кагу", + "PASS": "Туры килсә дә, бу кагыйдәне урап узу" } }, "system": { diff --git a/src/locales/zh.json b/src/locales/zh.json index f384a2eb..f451c8f1 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -87,6 +87,11 @@ "lock": "锁定菜单排序" } }, + "theme": { + "light": "浅色", + "dark": "深色", + "system": "系统" + }, "ruleEditor": { "title": "编辑规则", "form": { @@ -106,6 +111,41 @@ "conditionRequired": "规则条件缺失", "invalidRule": "无效规则" } + }, + "ruleTypes": { + "DOMAIN": "匹配完整域名", + "DOMAIN-SUFFIX": "匹配域名后缀", + "DOMAIN-KEYWORD": "匹配域名关键字", + "DOMAIN-REGEX": "匹配域名正则表达式", + "GEOSITE": "匹配 Geosite 内的域名", + "GEOIP": "匹配 IP 所属国家代码", + "SRC-GEOIP": "匹配来源 IP 所属国家代码", + "IP-ASN": "匹配 IP 所属 ASN", + "SRC-IP-ASN": "匹配来源 IP 所属 ASN", + "IP-CIDR": "匹配 IP 地址范围", + "IP-CIDR6": "匹配 IP 地址范围", + "SRC-IP-CIDR": "匹配来源 IP 地址范围", + "IP-SUFFIX": "匹配 IP 后缀范围", + "SRC-IP-SUFFIX": "匹配来源 IP 后缀范围", + "SRC-PORT": "匹配请求来源端口范围", + "DST-PORT": "匹配请求目标端口范围", + "IN-PORT": "匹配入站端口", + "DSCP": "DSCP标记(仅限 TPROXY UDP 入站)", + "PROCESS-NAME": "匹配进程名称(Android 包名)", + "PROCESS-PATH": "匹配完整进程路径", + "PROCESS-NAME-REGEX": "正则匹配完整进程名称(Android 包名)", + "PROCESS-PATH-REGEX": "正则匹配完整进程路径", + "NETWORK": "匹配传输协议 (TCP/UDP)", + "UID": "匹配 Linux USER ID", + "IN-TYPE": "匹配入站类型", + "IN-USER": "匹配入站用户名", + "IN-NAME": "匹配入站名称", + "SUB-RULE": "子规则", + "RULE-SET": "匹配规则集", + "AND": "逻辑和", + "OR": "逻辑或", + "NOT": "逻辑非", + "MATCH": "匹配所有请求" } }, "profile": { @@ -123,7 +163,7 @@ "name": "名称", "description": "描述", "subscriptionUrl": "订阅链接", - "httpTimeout": "HTTP Request Timeout", + "httpTimeout": "HTTP 请求超时", "updateInterval": "更新间隔", "useSystemProxy": "使用系统代理更新", "useClashProxy": "使用内核代理更新", @@ -204,8 +244,8 @@ }, "item": { "tooltips": { - "showLast": "Click to show last update time", - "showNext": "Click to show next update" + "showLast": "点击查看上次更新时间", + "showNext": "点击查看下一次更新" }, "status": { "lastUpdateFailed": "上次更新失败", @@ -226,8 +266,8 @@ }, "more": { "global": { - "merge": "Global Merge", - "script": "Global Script" + "merge": "全局扩展覆写配置", + "script": "全局扩展脚本" }, "chips": { "merge": "Merge", @@ -258,7 +298,7 @@ "fields": { "coreVersion": "内核版本", "systemProxyAddress": "系统代理地址", - "mixedPort": "Mixed Port", + "mixedPort": "混合代理端口", "uptime": "运行时间", "rulesCount": "规则数量" } @@ -292,10 +332,10 @@ "ipInfo": { "title": "IP 信息", "errors": { - "load": "获取IP信息失败" + "load": "获取 IP 信息失败" }, "labels": { - "retry": "Retry", + "retry": "重试", "ip": "IP", "asn": "自治域", "isp": "服务商", @@ -320,7 +360,7 @@ "download": "下载" }, "patterns": { - "minutes": "{{time}} Minutes" + "minutes": "{{time}} 分钟" } }, "currentProxy": { @@ -384,14 +424,14 @@ "communication": "内核通信错误" }, "labels": { - "rule": "规则模式", - "global": "全局模式", - "direct": "直连模式" + "rule": "规则", + "global": "全局", + "direct": "直连" }, "descriptions": { - "rule": "按照规则自动选择代理。", - "global": "将所有网络请求转发到选定的代理。", - "direct": "绕过代理,直接连接互联网。" + "rule": "基于预设规则智能判断流量走向,提供灵活的代理策略", + "global": "所有流量均通过代理服务器,适用于需要全局科学上网的场景", + "direct": "所有流量不经过代理节点,但经过Clash内核转发连接目标服务器,适用于需要通过内核进行分流的特定场景" } } }, @@ -415,7 +455,7 @@ "closeConnection": "关闭连接" }, "order": { - "default": "Default", + "default": "默认", "uploadSpeed": "上传速度", "downloadSpeed": "下载速度" } @@ -611,7 +651,7 @@ "backupTime": "备份时间", "actions": "操作", "noBackups": "暂无备份", - "rowsPerPage": "Rows per page" + "rowsPerPage": "每页行数" } }, "verge": { @@ -664,7 +704,7 @@ }, "notifications": { "latestVersion": "当前已是最新版本", - "versionCopied": "Verge版本已复制到剪贴板" + "versionCopied": "Verge 版本已复制到剪贴板" } }, "theme": { @@ -682,10 +722,10 @@ "cssInjection": "CSS 注入" }, "actions": { - "editCss": "Edit CSS" + "editCss": "编辑 CSS" }, "dialogs": { - "editCssTitle": "Edit CSS" + "editCssTitle": "编辑 CSS" } }, "layout": { @@ -910,7 +950,7 @@ "configError": "DNS 配置错误:" }, "errors": { - "invalid": "Invalid configuration" + "invalid": "配置无效" } }, "page": { @@ -964,8 +1004,8 @@ "updateSuccess": "{{name}} 更新成功", "updateFailed": "{{name}} 更新失败: {{message}}", "genericError": "更新失败: {{message}}", - "none": "没有可更新的提供者", - "allUpdated": "全部提供者更新成功" + "none": "没有可更新的 provider", + "allUpdated": "所有 provider 均已更新" } }, "test": { @@ -1009,6 +1049,11 @@ "default": "代理组", "chainMode": "链式代理模式" }, + "modes": { + "rule": "规则", + "global": "全局", + "direct": "直连" + }, "actions": { "toggleChain": "🔗 链式代理", "connect": "连接", @@ -1105,11 +1150,11 @@ "notifications": { "importRetry": "订阅导入失败,尝试使用 Clash 代理导入", "importFail": "使用 Clash 代理导入订阅也失败", - "importNeedsRefresh": "Profile imported but may need manual refresh", - "importSuccess": "Profile imported successfully, please restart if not visible", + "importNeedsRefresh": "订阅已导入,但可能需要手动刷新", + "importSuccess": "订阅已成功导入,如未显示请重启应用", "profileSwitched": "订阅已切换", "profileReactivated": "订阅已激活", - "switchInterrupted": "配置切换被新选择中断", + "switchInterrupted": "订阅切换被新选择中断", "batchDeleted": "选中的订阅已成功删除" }, "notices": { @@ -1180,10 +1225,16 @@ "proxy": { "strategies": { "select": "手动选择代理", - "url-test": "根据URL测试延迟选择代理", + "url-test": "根据 URL 测试延迟选择代理", "fallback": "不可用时切换到另一个代理", "load-balance": "根据负载均衡分配代理", "relay": "根据定义的代理链传递" + }, + "policies": { + "DIRECT": "直连", + "REJECT": "拦截请求", + "REJECT-DROP": "抛弃请求", + "PASS": "跳过此规则" } }, "system": { @@ -1232,11 +1283,11 @@ "fileError": "脚本文件错误,变更已撤销" }, "yaml": { - "syntaxError": "YAML语法错误,变更已撤销", - "readError": "YAML读取错误,变更已撤销", - "mappingError": "YAML映射错误,变更已撤销", - "keyError": "YAML键错误,变更已撤销", - "generalError": "YAML错误,变更已撤销" + "syntaxError": "YAML 语法错误,变更已撤销", + "readError": "YAML 读取错误,变更已撤销", + "mappingError": "YAML 映射错误,变更已撤销", + "keyError": "YAML 键错误,变更已撤销", + "generalError": "YAML 错误,变更已撤销" }, "merge": { "syntaxError": "覆写文件语法错误,变更已撤销", diff --git a/src/locales/zhtw.json b/src/locales/zhtw.json index 71743667..3525b828 100644 --- a/src/locales/zhtw.json +++ b/src/locales/zhtw.json @@ -87,6 +87,11 @@ "lock": "鎖定選單排序" } }, + "theme": { + "light": "淺色", + "dark": "深色", + "system": "系統" + }, "ruleEditor": { "title": "編輯規則", "form": { @@ -106,6 +111,41 @@ "conditionRequired": "規則條件為必填", "invalidRule": "無效規則" } + }, + "ruleTypes": { + "DOMAIN": "配對完整網域", + "DOMAIN-SUFFIX": "配對網域後綴", + "DOMAIN-KEYWORD": "配對網域關鍵字", + "DOMAIN-REGEX": "配對網域正規表示式", + "GEOSITE": "配對 Geosite 內的網域", + "GEOIP": "配對 IP 所屬國家代碼", + "SRC-GEOIP": "配對來源 IP 所屬國家代碼", + "IP-ASN": "配對 IP 所屬 ASN", + "SRC-IP-ASN": "配對來源 IP 所屬 ASN", + "IP-CIDR": "配對 IP 位址範圍", + "IP-CIDR6": "配對 IP 位址範圍", + "SRC-IP-CIDR": "配對來源 IP 位址範圍", + "IP-SUFFIX": "配對 IP 後綴範圍", + "SRC-IP-SUFFIX": "配對來源 IP 後綴範圍", + "SRC-PORT": "配對請求來源連接埠範圍", + "DST-PORT": "配對請求目標連接埠範圍", + "IN-PORT": "配對傳入連接埠", + "DSCP": "DSCP標記(僅限 TPROXY UDP 傳入)", + "PROCESS-NAME": "配對程序名稱(Android 應用程式套件名稱)", + "PROCESS-PATH": "配對完整程序路徑", + "PROCESS-NAME-REGEX": "正規表示式配對完整程序名稱(Android 應用程式套件名稱)", + "PROCESS-PATH-REGEX": "正規表示式配對完整程序路徑", + "NETWORK": "配對傳輸協定 (TCP/UDP)", + "UID": "配對 Linux 使用者 ID", + "IN-TYPE": "配對傳入類型", + "IN-USER": "配對傳入使用者名稱", + "IN-NAME": "配對傳入名稱", + "SUB-RULE": "子規則", + "RULE-SET": "配對規則集", + "AND": "邏輯 AND", + "OR": "邏輯 OR", + "NOT": "邏輯 NOT", + "MATCH": "配對所有請求" } }, "profile": { @@ -1009,6 +1049,11 @@ "default": "代理組", "chainMode": "鏈式代理模式" }, + "modes": { + "rule": "規則", + "global": "全局", + "direct": "直連" + }, "actions": { "toggleChain": "🔗 鏈式代理", "connect": "連線", @@ -1184,6 +1229,12 @@ "fallback": "切換至另一個備用代理", "load-balance": "根據負載平衡分配代理", "relay": "根據定義的代理鏈傳送" + }, + "policies": { + "DIRECT": "直連", + "REJECT": "拒絕請求", + "REJECT-DROP": "丟棄請求", + "PASS": "跳過此規則" } }, "system": {