mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-03 05:22:09 +08:00
chore: i18n (#5276)
* chore: notice i18n * feat: add script to clean up unused i18n keys * chore: cleanup i18n keys * refactor(i18n/proxies): migrate proxies UI to structured locale keys * chore: i18n for rule module * chore: i18n for profile module * chore: i18n for connections module * chore: i18n for settings module * chore: i18n for verge settings * chore: i18n for theme settings * chore: i18n for theme * chore(i18n): components.home.* * chore(i18n): remove unused i18n keys * chore(i18n): components.profile.* * chore(i18n): components.connection * chore(i18n): pages.logs.* * chore(i18n): pages.*.provider * chore(i18n): components.settings.externalCors.* * chore(i18n): components.settings.clash.* * chore(i18n): components.settings.liteMode.* * chore(i18n): components.settings.backup.* * chore(i18n): components.settings.clash.port.* * chore(i18n): components.settings.misc.* * chore(i18n): components.settings.update.* * chore(i18n): components.settings.sysproxy.* * chore(i18n): components.settings.sysproxy.* * chore(i18n): pages.profiles.notices/components.providers.notices * refactor(notice): unify showNotice usage * refactor(notice): add typed showNotice shortcuts, centralize defaults, and simplify subscriptions * refactor: unify showNotice usage * refactor(notice): unify showNotice API * refactor(notice): unify showNotice usage * chore(i18n): components.test.* * chore(i18n): components.settings.dns.* * chore(i18n): components.home.clashInfo.* * chore(i18n): components.home.systemInfo.* * chore(i18n): components.home.ipInfo/traffic.* * chore(i18n): navigation.* * refactor(i18n): remove pages.* namespace and migrate route texts under module-level page keys * chore(i18n): common.* * chore(i18n): common.* * fix: change error handling in patch_profiles_config to return false when a switch is in progress * fix: improve error handling in patch_profiles_config to prevent requests during profile switching * fix: change error handling in patch_profiles_config to return false when a switch is in progress fix: ensure CURRENT_SWITCHING_PROFILE is reset after config updates in perform_config_update and patch_profiles_config * chore(i18n): restructure root-level locale keys into namespaces * chore(i18n): add missing i18n keys * docs: i18n guide * chore: adjust i18n * refactor(i18n): align UI actions and status labels with common keys * refactor(i18n): unify two-name locale namespaces * refactor(i18n/components): unify locale keys and update component references * chore(i18n): add shared and entities namespaces to all locale files * refactor(i18n): consolidate shared and entity namespaces across features * chore(deps): update npm dependencies to ^7.3.5 (#5310) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * refactor(i18n): migrate shared editor modes and consolidate entities namespaces * tmp * refactor(i18n): flatten locales and move theme/validation strings * docs: CONTRIBUTING_i18n.md * refactor(i18n): restructure feedback and profile namespaces for better organization * refactor(i18n): unify settings locale structure and update references * refactor(i18n): reorganize locale keys for home, proxies, rules, connections, logs, unlock, and tests * refactor(i18n/feedback/layout): unify shared toasts & normalize layout namespace * refactor(i18n): centralize common UI strings in shared * refactor(i18n): flatten headers and unify locale schema * refactor(i18n): consolidate duplicate per-feature translations into shared namespace * refactor(i18n): split locales into per-namespace files * style: lint * refactor(i18n): unify unlock UI translations under tests namespace * feat(i18n): add type-checked translation keys * style: eslint import order * feat(i18n): replace ad-hoc loader with rust-i18n backend bundles * chore(prebuild): remove locale-copy step * fix(i18n, notice): propagate runtime params and update cleanup script path * fix(i18n,notice): make locale formatting idempotent and guard early notice translations * fix(i18n): resolve locale aliases and match OS codes correctly * fix(unlock): use i18next-compatible double-brace interpolation in failure notice * fix(i18n): route unlock error notices through translation keys * fix(i18n): i18n types * feat(i18n): localize upgrade notice for Clash core viewer * fix(notice): ensure runtime overrides apply to prefix translations * chore(i18n): replace literal notices with translation keys * chore(i18n): types * chore(i18n): regen typings before formatting to keep keys in sync * chore(i18n): simply labels * chore(i18n): adjust translation * chore: remove eslint-plugin-i18next * chore(i18n): add/refine Korean translations across frontend scopes and Rust backend (#5341) * chore(i18n): translate settings.json (missed in previous pass) (#5343) * chore(i18n): add/refine Korean translations across frontend scopes and Rust backend * chore(i18n): add/refine Korean translations across frontend scopes and Rust backend * fix(i18n-tauri): quote placeholder-leading value in ko.yml to prevent rust_i18n parse panic * chore(i18n): translate settings.json (forgot to include previously) --------- Co-authored-by: rozan <34974262+thelojan@users.noreply.github.com>
This commit is contained in:
co-authored by
renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
rozan
parent
c1a2e99f63
commit
c8aa72186e
@@ -35,10 +35,10 @@ export const ConfirmViewer = (props: Props) => {
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="outlined">
|
||||
{t("Cancel")}
|
||||
{t("shared.actions.cancel")}
|
||||
</Button>
|
||||
<Button onClick={onConfirm} variant="contained">
|
||||
{t("Confirm")}
|
||||
{t("shared.actions.confirm")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -97,7 +97,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
onClose,
|
||||
} = props;
|
||||
|
||||
const resolvedTitle = title ?? t("Edit File");
|
||||
const resolvedTitle = title ?? t("profiles.components.menu.editFile");
|
||||
const resolvedInitialData = useMemo(
|
||||
() => initialData ?? Promise.resolve(""),
|
||||
[initialData],
|
||||
@@ -130,8 +130,8 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
try {
|
||||
currData.current = value;
|
||||
onChange?.(prevData.current, currData.current);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -141,16 +141,16 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
onSave?.(prevData.current, currData.current);
|
||||
}
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
const handleClose = useLockFn(async () => {
|
||||
try {
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -200,7 +200,9 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
},
|
||||
mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
|
||||
readOnly: readOnly, // 只读模式
|
||||
readOnlyMessage: { value: t("ReadOnlyMessage") }, // 只读模式尝试编辑时的提示信息
|
||||
readOnlyMessage: {
|
||||
value: t("profiles.modals.editor.messages.readOnly"),
|
||||
}, // 只读模式尝试编辑时的提示信息
|
||||
renderValidationDecorations: "on", // 只读模式下显示校验信息
|
||||
quickSuggestions: {
|
||||
strings: true, // 字符串类型的建议
|
||||
@@ -229,7 +231,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
size="medium"
|
||||
color="inherit"
|
||||
sx={{ display: readOnly ? "none" : "" }}
|
||||
title={t("Format document")}
|
||||
title={t("profiles.modals.editor.actions.format")}
|
||||
onClick={() =>
|
||||
editorRef.current
|
||||
?.getAction("editor.action.formatDocument")
|
||||
@@ -241,7 +243,9 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
<IconButton
|
||||
size="medium"
|
||||
color="inherit"
|
||||
title={t(isMaximized ? "Minimize" : "Maximize")}
|
||||
title={t(
|
||||
isMaximized ? "shared.window.minimize" : "shared.window.maximize",
|
||||
)}
|
||||
onClick={() => appWindow.toggleMaximize().then(editorResize)}
|
||||
>
|
||||
{isMaximized ? <CloseFullscreenRounded /> : <OpenInFullRounded />}
|
||||
@@ -251,11 +255,11 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} variant="outlined">
|
||||
{t(readOnly ? "Close" : "Cancel")}
|
||||
{t(readOnly ? "shared.actions.close" : "shared.actions.cancel")}
|
||||
</Button>
|
||||
{!readOnly && (
|
||||
<Button onClick={handleSave} variant="contained">
|
||||
{t("Save")}
|
||||
{t("shared.actions.save")}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
|
||||
@@ -42,7 +42,7 @@ export const FileInput = (props: Props) => {
|
||||
sx={{ flex: "none" }}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
{t("Choose File")}
|
||||
{t("profiles.components.fileInput.chooseFile")}
|
||||
</Button>
|
||||
|
||||
<input
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
} from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { useThemeMode } from "@/services/states";
|
||||
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||
import getSystem from "@/utils/get-system";
|
||||
|
||||
import { BaseSearchBox } from "../base/base-search-box";
|
||||
@@ -73,6 +74,24 @@ interface Props {
|
||||
|
||||
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
|
||||
|
||||
const PROXY_STRATEGY_LABEL_KEYS: Record<string, TranslationKey> = {
|
||||
select: "proxies.components.enums.strategies.select",
|
||||
"url-test": "proxies.components.enums.strategies.url-test",
|
||||
fallback: "proxies.components.enums.strategies.fallback",
|
||||
"load-balance": "proxies.components.enums.strategies.load-balance",
|
||||
relay: "proxies.components.enums.strategies.relay",
|
||||
};
|
||||
|
||||
const PROXY_POLICY_LABEL_KEYS: Record<string, TranslationKey> =
|
||||
builtinProxyPolicies.reduce(
|
||||
(acc, policy) => {
|
||||
acc[policy] =
|
||||
`proxies.components.enums.policies.${policy}` as TranslationKey;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, TranslationKey>,
|
||||
);
|
||||
|
||||
const normalizeDeleteSeq = (input?: unknown): string[] => {
|
||||
if (!Array.isArray(input)) {
|
||||
return [];
|
||||
@@ -121,6 +140,20 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
const { mergeUid, proxiesUid, profileUid, property, open, onClose, onSave } =
|
||||
props;
|
||||
const { t } = useTranslation();
|
||||
const translateStrategy = useCallback(
|
||||
(value: string) =>
|
||||
PROXY_STRATEGY_LABEL_KEYS[value]
|
||||
? t(PROXY_STRATEGY_LABEL_KEYS[value])
|
||||
: value,
|
||||
[t],
|
||||
);
|
||||
const translatePolicy = useCallback(
|
||||
(value: string) =>
|
||||
PROXY_POLICY_LABEL_KEYS[value]
|
||||
? t(PROXY_POLICY_LABEL_KEYS[value])
|
||||
: value,
|
||||
[t],
|
||||
);
|
||||
const themeMode = useThemeMode();
|
||||
const [prevData, setPrevData] = useState("");
|
||||
const [currData, setCurrData] = useState("");
|
||||
@@ -369,7 +402,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
const validateGroup = () => {
|
||||
const group = formIns.getValues();
|
||||
if (group.name === "") {
|
||||
throw new Error(t("Group Name Required"));
|
||||
throw new Error(t("profiles.modals.groupsEditor.errors.nameRequired"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -384,12 +417,12 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
}
|
||||
|
||||
await saveProfileFile(property, nextData);
|
||||
showNotice("success", t("Saved Successfully"));
|
||||
showNotice.success("shared.feedback.notifications.saved");
|
||||
setPrevData(nextData);
|
||||
onSave?.(prevData, nextData);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -398,7 +431,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
<DialogTitle>
|
||||
{
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
{t("Edit Groups")}
|
||||
{t("profiles.modals.groupsEditor.title")}
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -407,7 +440,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
setVisualization((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
{visualization ? t("Advanced") : t("Visualization")}
|
||||
{visualization
|
||||
? t("shared.editorModes.advanced")
|
||||
: t("shared.editorModes.visualization")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -436,7 +471,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Group Type")} />
|
||||
<ListItemText
|
||||
primary={t("profiles.modals.groupsEditor.fields.type")}
|
||||
/>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ width: "calc(100% - 150px)" }}
|
||||
@@ -448,9 +485,10 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
"relay",
|
||||
]}
|
||||
value={field.value}
|
||||
getOptionLabel={translateStrategy}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} title={t(option)}>
|
||||
{option}
|
||||
<li {...props} title={translateStrategy(option)}>
|
||||
{translateStrategy(option)}
|
||||
</li>
|
||||
)}
|
||||
onChange={(_, value) => value && field.onChange(value)}
|
||||
@@ -464,7 +502,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Group Name")} />
|
||||
<ListItemText
|
||||
primary={t("profiles.modals.groupsEditor.fields.name")}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
size="small"
|
||||
@@ -481,7 +521,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Proxy Group Icon")} />
|
||||
<ListItemText
|
||||
primary={t("profiles.modals.groupsEditor.fields.icon")}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
size="small"
|
||||
@@ -496,7 +538,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Use Proxies")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.proxies",
|
||||
)}
|
||||
/>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{
|
||||
@@ -508,10 +554,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
onChange={(_, value) => value && field.onChange(value)}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} title={t(option)}>
|
||||
{option}
|
||||
<li {...props} title={translatePolicy(option)}>
|
||||
{translatePolicy(option)}
|
||||
</li>
|
||||
)}
|
||||
getOptionLabel={translatePolicy}
|
||||
/>
|
||||
</Item>
|
||||
)}
|
||||
@@ -521,7 +568,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Use Provider")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.provider",
|
||||
)}
|
||||
/>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ width: "calc(100% - 150px)" }}
|
||||
@@ -539,7 +590,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Health Check Url")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.healthCheckUrl",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
placeholder="https://cp.cloudflare.com/generate_204"
|
||||
@@ -555,7 +610,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Expected Status")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.expectedStatus",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
placeholder="*"
|
||||
@@ -573,7 +632,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Interval")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.interval",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
placeholder="300"
|
||||
@@ -587,7 +650,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{t("seconds")}
|
||||
{t("shared.units.seconds")}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
@@ -601,7 +664,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Timeout")} />
|
||||
<ListItemText primary={t("shared.labels.timeout")} />
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
placeholder="5000"
|
||||
@@ -615,7 +678,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{t("millis")}
|
||||
{t("shared.units.milliseconds")}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
@@ -629,7 +692,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Max Failed Times")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.maxFailedTimes",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
placeholder="5"
|
||||
@@ -648,7 +715,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Interface Name")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.interfaceName",
|
||||
)}
|
||||
/>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ width: "calc(100% - 150px)" }}
|
||||
@@ -665,7 +736,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Routing Mark")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.routingMark",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
type="number"
|
||||
@@ -683,7 +758,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Filter")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.filter",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
size="small"
|
||||
@@ -698,7 +777,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Exclude Filter")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.excludeFilter",
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
size="small"
|
||||
@@ -713,7 +796,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Exclude Type")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.excludeType",
|
||||
)}
|
||||
/>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={[
|
||||
@@ -759,7 +846,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Include All")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.includeAll",
|
||||
)}
|
||||
/>
|
||||
<Switch checked={field.value} {...field} />
|
||||
</Item>
|
||||
)}
|
||||
@@ -769,7 +860,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Include All Proxies")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.includeAllProxies",
|
||||
)}
|
||||
/>
|
||||
<Switch checked={field.value} {...field} />
|
||||
</Item>
|
||||
)}
|
||||
@@ -779,7 +874,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Include All Providers")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.fields.includeAllProviders",
|
||||
)}
|
||||
/>
|
||||
<Switch checked={field.value} {...field} />
|
||||
</Item>
|
||||
)}
|
||||
@@ -789,7 +888,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Lazy")} />
|
||||
<ListItemText
|
||||
primary={t("profiles.modals.groupsEditor.toggles.lazy")}
|
||||
/>
|
||||
<Switch checked={field.value} {...field} />
|
||||
</Item>
|
||||
)}
|
||||
@@ -799,7 +900,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Disable UDP")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.toggles.disableUdp",
|
||||
)}
|
||||
/>
|
||||
<Switch checked={field.value} {...field} />
|
||||
</Item>
|
||||
)}
|
||||
@@ -809,7 +914,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Item>
|
||||
<ListItemText primary={t("Hidden")} />
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"profiles.modals.groupsEditor.toggles.hidden",
|
||||
)}
|
||||
/>
|
||||
<Switch checked={field.value} {...field} />
|
||||
</Item>
|
||||
)}
|
||||
@@ -825,16 +934,18 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
validateGroup();
|
||||
for (const item of [...prependSeq, ...groupList]) {
|
||||
if (item.name === formIns.getValues().name) {
|
||||
throw new Error(t("Group Name Already Exists"));
|
||||
throw new Error(
|
||||
t("profiles.modals.groupsEditor.errors.nameExists"),
|
||||
);
|
||||
}
|
||||
}
|
||||
setPrependSeq([formIns.getValues(), ...prependSeq]);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("Prepend Group")}
|
||||
{t("profiles.modals.groupsEditor.actions.prepend")}
|
||||
</Button>
|
||||
</Item>
|
||||
<Item>
|
||||
@@ -847,16 +958,18 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
validateGroup();
|
||||
for (const item of [...appendSeq, ...groupList]) {
|
||||
if (item.name === formIns.getValues().name) {
|
||||
throw new Error(t("Group Name Already Exists"));
|
||||
throw new Error(
|
||||
t("profiles.modals.groupsEditor.errors.nameExists"),
|
||||
);
|
||||
}
|
||||
}
|
||||
setAppendSeq([...appendSeq, formIns.getValues()]);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("Append Group")}
|
||||
{t("profiles.modals.groupsEditor.actions.append")}
|
||||
</Button>
|
||||
</Item>
|
||||
</List>
|
||||
@@ -1007,11 +1120,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="outlined">
|
||||
{t("Cancel")}
|
||||
{t("shared.actions.cancel")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleSave} variant="contained">
|
||||
{t("Save")}
|
||||
{t("shared.actions.save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const LogViewer = (props: Props) => {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>{t("Script Console")}</DialogTitle>
|
||||
<DialogTitle>{t("profiles.modals.logViewer.title")}</DialogTitle>
|
||||
|
||||
<DialogContent
|
||||
sx={{
|
||||
@@ -62,7 +62,7 @@ export const LogViewer = (props: Props) => {
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="outlined">
|
||||
{t("Close")}
|
||||
{t("shared.actions.close")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
} from "@/services/cmds";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { useLoadingCache, useSetLoadingCache } from "@/services/states";
|
||||
import type { TranslationKey } from "@/types/generated/i18n-keys";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
import { ProfileBox } from "./profile-box";
|
||||
@@ -120,34 +121,46 @@ export const ProfileItem = (props: Props) => {
|
||||
|
||||
// 如果已经过期,显示"更新失败"
|
||||
if (nextUpdateDate.isBefore(now)) {
|
||||
setNextUpdateTime(t("Last Update failed"));
|
||||
setNextUpdateTime(
|
||||
t("profiles.components.profileItem.status.lastUpdateFailed"),
|
||||
);
|
||||
} else {
|
||||
// 否则显示剩余时间
|
||||
const diffMinutes = nextUpdateDate.diff(now, "minute");
|
||||
|
||||
if (diffMinutes < 60) {
|
||||
if (diffMinutes <= 0) {
|
||||
setNextUpdateTime(`${t("Next Up")} <1m`);
|
||||
setNextUpdateTime(
|
||||
`${t("profiles.components.profileItem.status.nextUp")} <1m`,
|
||||
);
|
||||
} else {
|
||||
setNextUpdateTime(`${t("Next Up")} ${diffMinutes}m`);
|
||||
setNextUpdateTime(
|
||||
`${t("profiles.components.profileItem.status.nextUp")} ${diffMinutes}m`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const hours = Math.floor(diffMinutes / 60);
|
||||
const mins = diffMinutes % 60;
|
||||
setNextUpdateTime(`${t("Next Up")} ${hours}h ${mins}m`);
|
||||
setNextUpdateTime(
|
||||
`${t("profiles.components.profileItem.status.nextUp")} ${hours}h ${mins}m`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`返回的下次更新时间为空`);
|
||||
setNextUpdateTime(t("No schedule"));
|
||||
setNextUpdateTime(
|
||||
t("profiles.components.profileItem.status.noSchedule"),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`获取下次更新时间出错:`, err);
|
||||
setNextUpdateTime(t("Unknown"));
|
||||
setNextUpdateTime(t("profiles.components.profileItem.status.unknown"));
|
||||
}
|
||||
} else {
|
||||
console.log(`该配置未设置更新间隔或间隔为0`);
|
||||
setNextUpdateTime(t("Auto update disabled"));
|
||||
setNextUpdateTime(
|
||||
t("profiles.components.profileItem.status.autoUpdateDisabled"),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -312,8 +325,8 @@ export const ProfileItem = (props: Props) => {
|
||||
setAnchorEl(null);
|
||||
try {
|
||||
await viewProfile(itemData.uid);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err?.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -354,42 +367,95 @@ export const ProfileItem = (props: Props) => {
|
||||
}
|
||||
});
|
||||
|
||||
const urlModeMenu = (
|
||||
hasHome ? [{ label: "Home", handler: onOpenHome, disabled: false }] : []
|
||||
).concat([
|
||||
{ label: "Select", handler: onForceSelect, disabled: false },
|
||||
{ label: "Edit Info", handler: onEditInfo, disabled: false },
|
||||
{ label: "Edit File", handler: onEditFile, disabled: false },
|
||||
type ContextMenuItem = {
|
||||
label: string;
|
||||
handler: () => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
const menuLabels: Record<string, TranslationKey> = {
|
||||
home: "profiles.components.menu.home",
|
||||
select: "profiles.components.menu.select",
|
||||
editInfo: "profiles.components.menu.editInfo",
|
||||
editFile: "profiles.components.menu.editFile",
|
||||
editRules: "profiles.components.menu.editRules",
|
||||
editProxies: "profiles.components.menu.editProxies",
|
||||
editGroups: "profiles.components.menu.editGroups",
|
||||
extendConfig: "profiles.components.menu.extendConfig",
|
||||
extendScript: "profiles.components.menu.extendScript",
|
||||
openFile: "profiles.components.menu.openFile",
|
||||
update: "profiles.components.menu.update",
|
||||
updateViaProxy: "profiles.components.menu.updateViaProxy",
|
||||
delete: "shared.actions.delete",
|
||||
} as const;
|
||||
|
||||
const urlModeMenu: ContextMenuItem[] = [
|
||||
...(hasHome
|
||||
? [
|
||||
{
|
||||
label: menuLabels.home,
|
||||
handler: onOpenHome,
|
||||
disabled: false,
|
||||
} satisfies ContextMenuItem,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: "Edit Rules",
|
||||
label: menuLabels.select,
|
||||
handler: onForceSelect,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: menuLabels.editInfo,
|
||||
handler: onEditInfo,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: menuLabels.editFile,
|
||||
handler: onEditFile,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: menuLabels.editRules,
|
||||
handler: onEditRules,
|
||||
disabled: !option?.rules,
|
||||
},
|
||||
{
|
||||
label: "Edit Proxies",
|
||||
label: menuLabels.editProxies,
|
||||
handler: onEditProxies,
|
||||
disabled: !option?.proxies,
|
||||
},
|
||||
{
|
||||
label: "Edit Groups",
|
||||
label: menuLabels.editGroups,
|
||||
handler: onEditGroups,
|
||||
disabled: !option?.groups,
|
||||
},
|
||||
{
|
||||
label: "Extend Config",
|
||||
label: menuLabels.extendConfig,
|
||||
handler: onEditMerge,
|
||||
disabled: !option?.merge,
|
||||
},
|
||||
{
|
||||
label: "Extend Script",
|
||||
label: menuLabels.extendScript,
|
||||
handler: onEditScript,
|
||||
disabled: !option?.script,
|
||||
},
|
||||
{ label: "Open File", handler: onOpenFile, disabled: false },
|
||||
{ label: "Update", handler: () => onUpdate(0), disabled: false },
|
||||
{ label: "Update via proxy", handler: () => onUpdate(2), disabled: false },
|
||||
{
|
||||
label: "Delete",
|
||||
label: menuLabels.openFile,
|
||||
handler: onOpenFile,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: menuLabels.update,
|
||||
handler: () => onUpdate(0),
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: menuLabels.updateViaProxy,
|
||||
handler: () => onUpdate(2),
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: menuLabels.delete,
|
||||
handler: () => {
|
||||
setAnchorEl(null);
|
||||
if (batchMode) {
|
||||
@@ -403,39 +469,55 @@ export const ProfileItem = (props: Props) => {
|
||||
},
|
||||
disabled: false,
|
||||
},
|
||||
]);
|
||||
const fileModeMenu = [
|
||||
{ label: "Select", handler: onForceSelect, disabled: false },
|
||||
{ label: "Edit Info", handler: onEditInfo, disabled: false },
|
||||
{ label: "Edit File", handler: onEditFile, disabled: false },
|
||||
];
|
||||
const fileModeMenu: ContextMenuItem[] = [
|
||||
{
|
||||
label: "Edit Rules",
|
||||
label: menuLabels.select,
|
||||
handler: onForceSelect,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: menuLabels.editInfo,
|
||||
handler: onEditInfo,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: menuLabels.editFile,
|
||||
handler: onEditFile,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: menuLabels.editRules,
|
||||
handler: onEditRules,
|
||||
disabled: !option?.rules,
|
||||
},
|
||||
{
|
||||
label: "Edit Proxies",
|
||||
label: menuLabels.editProxies,
|
||||
handler: onEditProxies,
|
||||
disabled: !option?.proxies,
|
||||
},
|
||||
{
|
||||
label: "Edit Groups",
|
||||
label: menuLabels.editGroups,
|
||||
handler: onEditGroups,
|
||||
disabled: !option?.groups,
|
||||
},
|
||||
{
|
||||
label: "Extend Config",
|
||||
label: menuLabels.extendConfig,
|
||||
handler: onEditMerge,
|
||||
disabled: !option?.merge,
|
||||
},
|
||||
{
|
||||
label: "Extend Script",
|
||||
label: menuLabels.extendScript,
|
||||
handler: onEditScript,
|
||||
disabled: !option?.script,
|
||||
},
|
||||
{ label: "Open File", handler: onOpenFile, disabled: false },
|
||||
{
|
||||
label: "Delete",
|
||||
label: menuLabels.openFile,
|
||||
handler: onOpenFile,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: menuLabels.delete,
|
||||
handler: () => {
|
||||
setAnchorEl(null);
|
||||
if (batchMode) {
|
||||
@@ -599,7 +681,7 @@ export const ProfileItem = (props: Props) => {
|
||||
{/* only if has url can it be updated */}
|
||||
{hasUrl && (
|
||||
<IconButton
|
||||
title={t("Refresh")}
|
||||
title={t("shared.actions.refresh")}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
p: "3px",
|
||||
@@ -637,7 +719,10 @@ export const ProfileItem = (props: Props) => {
|
||||
</Typography>
|
||||
) : (
|
||||
hasUrl && (
|
||||
<Typography noWrap title={`${t("From")} ${from}`}>
|
||||
<Typography
|
||||
noWrap
|
||||
title={`${t("shared.labels.from")} ${from}`}
|
||||
>
|
||||
{from}
|
||||
</Typography>
|
||||
)
|
||||
@@ -657,8 +742,8 @@ export const ProfileItem = (props: Props) => {
|
||||
textAlign="right"
|
||||
title={
|
||||
showNextUpdate
|
||||
? t("Click to show last update time")
|
||||
: `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`
|
||||
? t("profiles.components.profileItem.tooltips.showLast")
|
||||
: `${t("shared.labels.updateTime")}: ${parseExpire(updated)}\n${t("profiles.components.profileItem.tooltips.showNext")}`
|
||||
}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
@@ -686,14 +771,16 @@ export const ProfileItem = (props: Props) => {
|
||||
{/* the third line show extra info or last updated time */}
|
||||
{hasExtra ? (
|
||||
<Box sx={{ ...boxStyle, fontSize: 14 }}>
|
||||
<span title={t("Used / Total")}>
|
||||
<span title={t("shared.labels.usedTotal")}>
|
||||
{parseTraffic(upload + download)} / {parseTraffic(total)}
|
||||
</span>
|
||||
<span title={t("Expire Time")}>{expire}</span>
|
||||
<span title={t("shared.labels.expireTime")}>{expire}</span>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ ...boxStyle, fontSize: 12, justifyContent: "flex-end" }}>
|
||||
<span title={t("Update Time")}>{parseExpire(updated)}</span>
|
||||
<span title={t("shared.labels.updateTime")}>
|
||||
{parseExpire(updated)}
|
||||
</span>
|
||||
</Box>
|
||||
)}
|
||||
<LinearProgress
|
||||
@@ -728,7 +815,7 @@ export const ProfileItem = (props: Props) => {
|
||||
(theme) => {
|
||||
return {
|
||||
color:
|
||||
item.label === "Delete"
|
||||
item.label === menuLabels.delete
|
||||
? theme.palette.error.main
|
||||
: undefined,
|
||||
};
|
||||
@@ -813,8 +900,8 @@ export const ProfileItem = (props: Props) => {
|
||||
)}
|
||||
|
||||
<ConfirmViewer
|
||||
title={t("Confirm deletion")}
|
||||
message={t("This operation is not reversible")}
|
||||
title={t("profiles.modals.confirmDelete.title")}
|
||||
message={t("profiles.modals.confirmDelete.message")}
|
||||
open={confirmOpen}
|
||||
onClose={() => setConfirmOpen(false)}
|
||||
onConfirm={() => {
|
||||
|
||||
@@ -47,16 +47,26 @@ export const ProfileMore = (props: Props) => {
|
||||
setAnchorEl(null);
|
||||
try {
|
||||
await viewProfile(id);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err?.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
const hasError = entries.some(([level]) => level === "exception");
|
||||
|
||||
const globalTitles: Record<Props["id"], string> = {
|
||||
Merge: "profiles.components.more.global.merge",
|
||||
Script: "profiles.components.more.global.script",
|
||||
};
|
||||
|
||||
const chipLabels: Record<Props["id"], string> = {
|
||||
Merge: "profiles.components.more.chips.merge",
|
||||
Script: "profiles.components.more.chips.script",
|
||||
};
|
||||
|
||||
const itemMenu = [
|
||||
{ label: "Edit File", handler: onEditFile },
|
||||
{ label: "Open File", handler: onOpenFile },
|
||||
{ label: "profiles.components.menu.editFile", handler: onEditFile },
|
||||
{ label: "profiles.components.menu.openFile", handler: onOpenFile },
|
||||
];
|
||||
|
||||
const boxStyle = {
|
||||
@@ -89,13 +99,13 @@ export const ProfileMore = (props: Props) => {
|
||||
variant="h6"
|
||||
component="h2"
|
||||
noWrap
|
||||
title={t(`Global ${id}`)}
|
||||
title={t(globalTitles[id])}
|
||||
>
|
||||
{t(`Global ${id}`)}
|
||||
{t(globalTitles[id])}
|
||||
</Typography>
|
||||
|
||||
<Chip
|
||||
label={id}
|
||||
label={t(chipLabels[id])}
|
||||
color="primary"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
@@ -111,7 +121,7 @@ export const ProfileMore = (props: Props) => {
|
||||
size="small"
|
||||
edge="start"
|
||||
color="error"
|
||||
title={t("Script Console")}
|
||||
title={t("profiles.modals.logViewer.title")}
|
||||
onClick={() => setLogOpen(true)}
|
||||
>
|
||||
<FeaturedPlayListRounded fontSize="inherit" />
|
||||
@@ -122,7 +132,7 @@ export const ProfileMore = (props: Props) => {
|
||||
size="small"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
title={t("Script Console")}
|
||||
title={t("profiles.modals.logViewer.title")}
|
||||
onClick={() => setLogOpen(true)}
|
||||
>
|
||||
<FeaturedPlayListRounded fontSize="inherit" />
|
||||
@@ -170,7 +180,7 @@ export const ProfileMore = (props: Props) => {
|
||||
{fileOpen && (
|
||||
<EditorViewer
|
||||
open={true}
|
||||
title={`${t("Global " + id)}`}
|
||||
title={t(globalTitles[id])}
|
||||
initialData={readProfileFile(id)}
|
||||
language={id === "Merge" ? "yaml" : "javascript"}
|
||||
schema={id === "Merge" ? "clash" : undefined}
|
||||
|
||||
@@ -144,9 +144,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
}
|
||||
} catch {
|
||||
// 首次创建/更新失败,尝试使用自身代理
|
||||
showNotice(
|
||||
"info",
|
||||
t("Profile creation failed, retrying with Clash proxy..."),
|
||||
showNotice.info(
|
||||
"profiles.modals.profileForm.feedback.notifications.creationRetry",
|
||||
);
|
||||
|
||||
// 使用自身代理的配置
|
||||
@@ -170,9 +169,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
await patchProfile(form.uid, { option: originalOptions });
|
||||
}
|
||||
|
||||
showNotice(
|
||||
"success",
|
||||
t("Profile creation succeeded with Clash proxy"),
|
||||
showNotice.success(
|
||||
"profiles.modals.profileForm.feedback.notifications.creationSuccess",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -186,8 +184,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
setTimeout(() => {
|
||||
onChange(isActivating);
|
||||
}, 0);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -220,10 +218,14 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={openType === "new" ? t("Create Profile") : t("Edit Profile")}
|
||||
title={
|
||||
openType === "new"
|
||||
? t("profiles.modals.profileForm.title.create")
|
||||
: t("profiles.modals.profileForm.title.edit")
|
||||
}
|
||||
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
okBtn={t("shared.actions.save")}
|
||||
cancelBtn={t("shared.actions.cancel")}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
onOk={handleOk}
|
||||
@@ -234,8 +236,14 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl size="small" fullWidth sx={{ mt: 1, mb: 1 }}>
|
||||
<InputLabel>{t("Type")}</InputLabel>
|
||||
<Select {...field} autoFocus label={t("Type")}>
|
||||
<InputLabel>
|
||||
{t("profiles.modals.profileForm.fields.type")}
|
||||
</InputLabel>
|
||||
<Select
|
||||
{...field}
|
||||
autoFocus
|
||||
label={t("profiles.modals.profileForm.fields.type")}
|
||||
>
|
||||
<MenuItem value="remote">Remote</MenuItem>
|
||||
<MenuItem value="local">Local</MenuItem>
|
||||
</Select>
|
||||
@@ -247,7 +255,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField {...text} {...field} label={t("Name")} />
|
||||
<TextField {...text} {...field} label={t("shared.labels.name")} />
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -255,7 +263,11 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
name="desc"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField {...text} {...field} label={t("Descriptions")} />
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
label={t("profiles.modals.profileForm.fields.description")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -269,7 +281,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
{...text}
|
||||
{...field}
|
||||
multiline
|
||||
label={t("Subscription URL")}
|
||||
label={t("profiles.modals.profileForm.fields.subscriptionUrl")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -296,12 +308,12 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
{...field}
|
||||
type="number"
|
||||
placeholder="60"
|
||||
label={t("HTTP Request Timeout")}
|
||||
label={t("profiles.modals.profileForm.fields.httpTimeout")}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{t("seconds")}
|
||||
{t("shared.units.seconds")}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
@@ -321,11 +333,13 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
{...text}
|
||||
{...field}
|
||||
type="number"
|
||||
label={t("Update Interval")}
|
||||
label={t("profiles.modals.profileForm.fields.updateInterval")}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{t("mins")}</InputAdornment>
|
||||
<InputAdornment position="end">
|
||||
{t("shared.units.minutes")}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
@@ -350,7 +364,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Use System Proxy")}</InputLabel>
|
||||
<InputLabel>
|
||||
{t("profiles.modals.profileForm.fields.useSystemProxy")}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
@@ -361,7 +377,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Use Clash Proxy")}</InputLabel>
|
||||
<InputLabel>
|
||||
{t("profiles.modals.profileForm.fields.useClashProxy")}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
@@ -372,7 +390,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Accept Invalid Certs (Danger)")}</InputLabel>
|
||||
<InputLabel>
|
||||
{t("profiles.modals.profileForm.fields.acceptInvalidCerts")}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
@@ -383,7 +403,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Allow Auto Update")}</InputLabel>
|
||||
<InputLabel>
|
||||
{t("profiles.modals.profileForm.fields.allowAutoUpdate")}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
|
||||
@@ -163,11 +163,11 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
proxies.push(proxy);
|
||||
names.push(proxy.name);
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[ProxiesEditorViewer] parseUri failed for line:",
|
||||
uri,
|
||||
err?.message || err,
|
||||
err,
|
||||
);
|
||||
// 不阻塞主流程
|
||||
}
|
||||
@@ -263,11 +263,11 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
const handleSave = useLockFn(async () => {
|
||||
try {
|
||||
await saveProfileFile(property, currData);
|
||||
showNotice("success", t("Saved Successfully"));
|
||||
showNotice.success("shared.feedback.notifications.saved");
|
||||
onSave?.(prevData, currData);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.toString());
|
||||
} catch (err) {
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -276,7 +276,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
<DialogTitle>
|
||||
{
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
{t("Edit Proxies")}
|
||||
{t("profiles.modals.proxiesEditor.title")}
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -285,7 +285,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
setVisualization((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
{visualization ? t("Advanced") : t("Visualization")}
|
||||
{visualization
|
||||
? t("shared.editorModes.advanced")
|
||||
: t("shared.editorModes.visualization")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -312,7 +314,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
<Item>
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
placeholder={t("Use newlines for multiple uri")}
|
||||
placeholder={t(
|
||||
"profiles.modals.proxiesEditor.placeholders.multiUri",
|
||||
)}
|
||||
fullWidth
|
||||
rows={9}
|
||||
multiline
|
||||
@@ -332,7 +336,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("Prepend Proxy")}
|
||||
{t("profiles.modals.proxiesEditor.actions.prepend")}
|
||||
</Button>
|
||||
</Item>
|
||||
<Item>
|
||||
@@ -346,7 +350,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("Append Proxy")}
|
||||
{t("profiles.modals.proxiesEditor.actions.append")}
|
||||
</Button>
|
||||
</Item>
|
||||
</List>
|
||||
@@ -497,11 +501,11 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="outlined">
|
||||
{t("Cancel")}
|
||||
{t("shared.actions.cancel")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleSave} variant="contained">
|
||||
{t("Save")}
|
||||
{t("shared.actions.save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -240,8 +240,24 @@ const rules: {
|
||||
},
|
||||
];
|
||||
|
||||
const RULE_TYPE_LABEL_KEYS: Record<string, string> = Object.fromEntries(
|
||||
rules.map((rule) => [
|
||||
rule.name,
|
||||
`rules.modals.editor.ruleTypes.${rule.name}`,
|
||||
]),
|
||||
);
|
||||
|
||||
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
|
||||
|
||||
const PROXY_POLICY_LABEL_KEYS: Record<string, string> =
|
||||
builtinProxyPolicies.reduce(
|
||||
(acc, policy) => {
|
||||
acc[policy] = `proxy.policies.${policy}`;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
export const RulesEditorViewer = (props: Props) => {
|
||||
const { groupsUid, mergeUid, profileUid, property, open, onClose, onSave } =
|
||||
props;
|
||||
@@ -350,8 +366,8 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
{ forceQuotes: true },
|
||||
),
|
||||
);
|
||||
} catch (e: any) {
|
||||
showNotice("error", e?.message || e?.toString() || "YAML dump error");
|
||||
} catch (error) {
|
||||
showNotice.error(error ?? "YAML dump error");
|
||||
}
|
||||
};
|
||||
let idleId: number | undefined;
|
||||
@@ -462,10 +478,12 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
|
||||
const validateRule = () => {
|
||||
if ((ruleType.required ?? true) && !ruleContent) {
|
||||
throw new Error(t("Rule Condition Required"));
|
||||
throw new Error(
|
||||
t("rules.modals.editor.form.validation.conditionRequired"),
|
||||
);
|
||||
}
|
||||
if (ruleType.validator && !ruleType.validator(ruleContent)) {
|
||||
throw new Error(t("Invalid Rule"));
|
||||
throw new Error(t("rules.modals.editor.form.validation.invalidRule"));
|
||||
}
|
||||
|
||||
const condition = (ruleType.required ?? true) ? ruleContent : "";
|
||||
@@ -477,11 +495,11 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
const handleSave = useLockFn(async () => {
|
||||
try {
|
||||
await saveProfileFile(property, currData);
|
||||
showNotice("success", t("Saved Successfully"));
|
||||
showNotice.success("shared.feedback.notifications.saved");
|
||||
onSave?.(prevData, currData);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.toString());
|
||||
showNotice.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -490,7 +508,7 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
<DialogTitle>
|
||||
{
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
{t("Edit Rules")}
|
||||
{t("rules.modals.editor.title")}
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -499,7 +517,9 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
setVisualization((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
{visualization ? t("Advanced") : t("Visualization")}
|
||||
{visualization
|
||||
? t("shared.editorModes.advanced")
|
||||
: t("shared.editorModes.visualization")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -518,26 +538,37 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<Item>
|
||||
<ListItemText primary={t("Rule Type")} />
|
||||
<ListItemText
|
||||
primary={t("rules.modals.editor.form.labels.type")}
|
||||
/>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ minWidth: "240px" }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
options={rules}
|
||||
value={ruleType}
|
||||
getOptionLabel={(option) => option.name}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} title={t(option.name)}>
|
||||
{option.name}
|
||||
</li>
|
||||
)}
|
||||
getOptionLabel={(option) =>
|
||||
t(RULE_TYPE_LABEL_KEYS[option.name] ?? option.name)
|
||||
}
|
||||
renderOption={(props, option) => {
|
||||
const label = t(
|
||||
RULE_TYPE_LABEL_KEYS[option.name] ?? option.name,
|
||||
);
|
||||
return (
|
||||
<li {...props} title={label}>
|
||||
{label}
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
onChange={(_, value) => value && setRuleType(value)}
|
||||
/>
|
||||
</Item>
|
||||
<Item
|
||||
sx={{ display: !(ruleType.required ?? true) ? "none" : "" }}
|
||||
>
|
||||
<ListItemText primary={t("Rule Content")} />
|
||||
<ListItemText
|
||||
primary={t("rules.modals.editor.form.labels.content")}
|
||||
/>
|
||||
|
||||
{ruleType.name === "RULE-SET" && (
|
||||
<Autocomplete
|
||||
@@ -574,24 +605,34 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
)}
|
||||
</Item>
|
||||
<Item>
|
||||
<ListItemText primary={t("Proxy Policy")} />
|
||||
<ListItemText
|
||||
primary={t("rules.modals.editor.form.labels.proxyPolicy")}
|
||||
/>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ minWidth: "240px" }}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
options={proxyPolicyList}
|
||||
value={proxyPolicy}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} title={t(option)}>
|
||||
{option}
|
||||
</li>
|
||||
)}
|
||||
getOptionLabel={(option) =>
|
||||
t(PROXY_POLICY_LABEL_KEYS[option] ?? option)
|
||||
}
|
||||
renderOption={(props, option) => {
|
||||
const label = t(PROXY_POLICY_LABEL_KEYS[option] ?? option);
|
||||
return (
|
||||
<li {...props} title={label}>
|
||||
{label}
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
onChange={(_, value) => value && setProxyPolicy(value)}
|
||||
/>
|
||||
</Item>
|
||||
{ruleType.noResolve && (
|
||||
<Item>
|
||||
<ListItemText primary={t("No Resolve")} />
|
||||
<ListItemText
|
||||
primary={t("rules.modals.editor.form.toggles.noResolve")}
|
||||
/>
|
||||
<Switch
|
||||
checked={noResolve}
|
||||
onChange={() => setNoResolve(!noResolve)}
|
||||
@@ -609,11 +650,11 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
if (prependSeq.includes(raw)) return;
|
||||
setPrependSeq([raw, ...prependSeq]);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
showNotice.error(err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("Prepend Rule")}
|
||||
{t("rules.modals.editor.form.actions.prependRule")}
|
||||
</Button>
|
||||
</Item>
|
||||
<Item>
|
||||
@@ -627,11 +668,11 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
if (appendSeq.includes(raw)) return;
|
||||
setAppendSeq([...appendSeq, raw]);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
showNotice.error(err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("Append Rule")}
|
||||
{t("rules.modals.editor.form.actions.appendRule")}
|
||||
</Button>
|
||||
</Item>
|
||||
</List>
|
||||
@@ -776,11 +817,11 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="outlined">
|
||||
{t("Cancel")}
|
||||
{t("shared.actions.cancel")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleSave} variant="contained">
|
||||
{t("Save")}
|
||||
{t("shared.actions.save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user