diff --git a/Changelog.md b/Changelog.md index d0165c2a0..d01e4a0c2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,7 @@ - macOS 非预期 Tproxy 端口设置 - 流量图缩放异常 - PAC 自动代理脚本内容无法动态调整 +- Monaco 编辑器的行数上限
✨ 新增功能 diff --git a/src/components/base/base-loading-overlay.tsx b/src/components/base/base-loading-overlay.tsx index 98727dcb1..a115da25c 100644 --- a/src/components/base/base-loading-overlay.tsx +++ b/src/components/base/base-loading-overlay.tsx @@ -21,7 +21,11 @@ export const BaseLoadingOverlay: React.FC = ({ display: "flex", justifyContent: "center", alignItems: "center", - backgroundColor: "rgba(255, 255, 255, 0.7)", + // Respect current theme; avoid bright flash in dark mode + backgroundColor: (theme) => + theme.palette.mode === "dark" + ? "rgba(0, 0, 0, 0.5)" + : "rgba(255, 255, 255, 0.7)", zIndex: 1000, }} > diff --git a/src/components/profile/editor-viewer.tsx b/src/components/profile/editor-viewer.tsx index 64f2e1675..d5d16495b 100644 --- a/src/components/profile/editor-viewer.tsx +++ b/src/components/profile/editor-viewer.tsx @@ -21,10 +21,11 @@ import metaSchema from "meta-json-schema/schemas/meta-json-schema.json"; import * as monaco from "monaco-editor"; import { configureMonacoYaml } from "monaco-yaml"; import { nanoid } from "nanoid"; -import { ReactNode, useEffect, useMemo, useRef, useState } from "react"; +import { ReactNode, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import pac from "types-pac/pac.d.ts?raw"; +import { BaseLoadingOverlay } from "@/components/base"; import { showNotice } from "@/services/noticeService"; import { useThemeMode } from "@/services/states"; import debounce from "@/utils/debounce"; @@ -42,12 +43,16 @@ interface LanguageSchemaMap { interface Props { open: boolean; title?: string | ReactNode; - initialData: Promise; + // Initial content loader: prefer passing a stable function. A plain Promise is supported, + // but it won't trigger background refreshes and should be paired with a stable `dataKey`. + initialData: Promise | (() => Promise); + // Logical document id; reloads when this or language/schema changes. + dataKey?: string | number; readOnly?: boolean; language: T; schema?: Schema; onChange?: (prev?: string, curr?: string) => void; - onSave?: (prev?: string, curr?: string) => void; + onSave?: (prev?: string, curr?: string) => void | Promise; onClose: () => void; } @@ -55,7 +60,7 @@ let initialized = false; const monacoInitialization = () => { if (initialized) return; - // configure yaml worker + // YAML worker and schemas configureMonacoYaml(monaco, { validate: true, enableSchemaRequest: true, @@ -74,7 +79,7 @@ const monacoInitialization = () => { }, ], }); - // configure PAC definition + // PAC type definitions for JS suggestions monaco.languages.typescript.javascriptDefaults.addExtraLib(pac, "pac.d.ts"); initialized = true; @@ -89,6 +94,7 @@ export const EditorViewer = (props: Props) => { open = false, title, initialData, + dataKey, readOnly = false, language = "yaml", schema, @@ -98,39 +104,223 @@ export const EditorViewer = (props: Props) => { } = props; const resolvedTitle = title ?? t("profiles.components.menu.editFile"); - const resolvedInitialData = useMemo( - () => initialData ?? Promise.resolve(""), - [initialData], - ); const editorRef = useRef(undefined); const prevData = useRef(""); const currData = useRef(""); + // Hold the latest loader without making effects depend on its identity + const initialDataRef = useRef["initialData"]>(initialData); + // Track mount/open state to prevent setState after unmount/close + const isMountedRef = useRef(true); + const openRef = useRef(open); + useEffect(() => { + openRef.current = open; + }, [open]); + useEffect(() => { + isMountedRef.current = true; + return () => { + isMountedRef.current = false; + }; + }, []); + const [initialText, setInitialText] = useState(null); + const [modelPath, setModelPath] = useState(""); + const modelChangeDisposableRef = useRef(null); + // Unique per-component instance id to avoid shared Monaco models across dialogs + const instanceIdRef = useRef(nanoid()); + // Disable actions while loading or before modelPath is ready + const isLoading = initialText === null || !modelPath; + // Track if background refresh failed; offer a retry action in UI + const [refreshFailed, setRefreshFailed] = useState(null); + // Skip the first background refresh triggered by [open, modelPath, dataKey] + // to avoid double-invoking the loader right after the initial load. + const skipNextRefreshRef = useRef(false); + // Monotonic token to cancel stale background refreshes + const reloadTokenRef = useRef(0); + // Track whether the editor has a usable baseline (either loaded or fallback). + // This avoids saving before the model/path are ready, while still allowing recovery + // when the initial load fails but an empty buffer is presented. + const [hasLoadedOnce, setHasLoadedOnce] = useState(false); + // Editor should only be read-only when explicitly requested by prop. + // A refresh/load failure must not lock the editor to allow manual recovery. + const effectiveReadOnly = readOnly; + // Keep ref in sync with prop without triggering loads + useEffect(() => { + initialDataRef.current = initialData; + }, [initialData]); + // Background refresh: when the dialog/model is ready and the underlying resource key changes, + // try to refresh content (only if user hasn't typed). Do NOT depend on `initialData` function + // identity because callers often pass inline lambdas that change every render. + useEffect(() => { + if (!open) return; + // Only attempt after initial model is ready to avoid racing the initial load + if (!modelPath) return; + // Avoid immediate double-load on open: the initial load has just completed. + if (skipNextRefreshRef.current) { + skipNextRefreshRef.current = false; + return; + } + // Only meaningful when a callable loader is provided (plain Promise cannot be "recalled") + if (typeof initialDataRef.current === "function") { + void reloadLatest(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, modelPath, dataKey]); + // Helper to (soft) reload latest source and apply only if the user hasn't typed yet + const reloadLatest = useLockFn(async () => { + // Snapshot the model/doc identity and bump a token so older calls can't win + const myToken = ++reloadTokenRef.current; + const expectedModelPath = modelPath; + const expectedKey = dataKey; + if (isMountedRef.current && openRef.current) { + // Clear previous error (UI hint) at the start of a new attempt + setRefreshFailed(null); + } + try { + const src = initialDataRef.current; + const promise = + typeof src === "function" + ? (src as () => Promise)() + : (src ?? Promise.resolve("")); + const next = await promise; + // Abort if component/dialog state changed meanwhile: + // - unmounted or closed + // - document switched (modelPath/dataKey no longer match) + // - a newer reload was started + if ( + !isMountedRef.current || + !openRef.current || + expectedModelPath !== modelPath || + expectedKey !== dataKey || + myToken !== reloadTokenRef.current + ) { + return; + } + // Only update when untouched and value changed + const userUntouched = currData.current === prevData.current; + if (userUntouched && next !== prevData.current) { + prevData.current = next; + currData.current = next; + editorRef.current?.setValue(next); + } + // Ensure any previous error state is cleared after a successful refresh + if (isMountedRef.current && openRef.current) { + setRefreshFailed(null); + } + // If we previously failed to load, a successful refresh establishes a valid baseline + if (isMountedRef.current && openRef.current) { + setHasLoadedOnce(true); + } + } catch (err) { + // Only report if still mounted/open and this call is the latest + if ( + isMountedRef.current && + openRef.current && + myToken === reloadTokenRef.current + ) { + setRefreshFailed(err ?? true); + showNotice.error( + "shared.feedback.notifications.common.refreshFailed", + err, + ); + } + } + }); const beforeMount = () => { - monacoInitialization(); // initialize monaco + monacoInitialization(); }; + // Prepare initial content and a stable model path for monaco-react + /* eslint-disable @eslint-react/hooks-extra/no-direct-set-state-in-use-effect */ + useEffect(() => { + if (!open) return; + let cancelled = false; + // Clear state up-front to avoid showing stale content while loading + setInitialText(null); + setModelPath(""); + // Clear any stale refresh error when starting a new load + setRefreshFailed(null); + // Reset initial-load success flag on open/start + setHasLoadedOnce(false); + // We will perform an explicit initial load below; skip the first background refresh. + skipNextRefreshRef.current = true; + prevData.current = undefined; + currData.current = undefined; + + (async () => { + try { + const dataSource = initialDataRef.current; + const dataPromise = + typeof dataSource === "function" + ? (dataSource as () => Promise)() + : (dataSource ?? Promise.resolve("")); + const data = await dataPromise; + if (cancelled) return; + prevData.current = data; + currData.current = data; + + setInitialText(data); + // Build a path that matches YAML schemas when applicable, and avoids "undefined" in name + const pathParts = [String(dataKey ?? nanoid()), instanceIdRef.current]; + if (schema) pathParts.push(String(schema)); + pathParts.push(language); + + setModelPath(pathParts.join(".")); + // Successful initial load should clear any previous refresh error flag + setRefreshFailed(null); + // Mark that we have a valid baseline content + setHasLoadedOnce(true); + } catch (err) { + if (cancelled) return; + // Notify the error and still show an empty editor so the user isn't stuck + showNotice.error(err); + + // Align refs with fallback text after a load failure + prevData.current = ""; + currData.current = ""; + + setInitialText(""); + const pathParts = [String(dataKey ?? nanoid()), instanceIdRef.current]; + if (schema) pathParts.push(String(schema)); + pathParts.push(language); + + setModelPath(pathParts.join(".")); + // Mark refresh failure so users can retry + setRefreshFailed(err ?? true); + // Initial load failed; keep `hasLoadedOnce` false to prevent accidental save + // of an empty buffer. It will be enabled on successful refresh or first edit. + setHasLoadedOnce(false); + } + })(); + + return () => { + cancelled = true; + }; + }, [open, dataKey, language, schema]); + /* eslint-enable @eslint-react/hooks-extra/no-direct-set-state-in-use-effect */ + const onMount = async (editor: monaco.editor.IStandaloneCodeEditor) => { editorRef.current = editor; - - // retrieve initial data - await resolvedInitialData.then((data) => { - prevData.current = data; - currData.current = data; - - // create and set model - const uri = monaco.Uri.parse(`${nanoid()}.${schema}.${language}`); - const model = monaco.editor.createModel(data, language, uri); - editorRef.current?.setModel(model); + // Dispose previous model when switching (monaco-react creates a fresh model when `path` changes) + modelChangeDisposableRef.current?.dispose(); + modelChangeDisposableRef.current = editor.onDidChangeModel((e) => { + if (e.oldModelUrl) { + const oldModel = monaco.editor.getModel(e.oldModelUrl); + oldModel?.dispose(); + } }); + // No refresh on mount; doing so would double-load. + // Background refreshes are handled by the [open, modelPath, dataKey] effect. }; - const handleChange = useLockFn(async (_value?: string) => { + const handleChange = useLockFn(async (value?: string) => { try { - const value = editorRef.current?.getValue(); - currData.current = value; + currData.current = value ?? editorRef.current?.getValue(); onChange?.(prevData.current, currData.current); + // If the initial load failed, allow saving after the user makes an edit. + if (!hasLoadedOnce) { + setHasLoadedOnce(true); + } } catch (err) { showNotice.error(err); } @@ -138,9 +328,18 @@ export const EditorViewer = (props: Props) => { const handleSave = useLockFn(async () => { try { - if (!readOnly) { - currData.current = editorRef.current?.getValue(); - onSave?.(prevData.current, currData.current); + // Disallow saving if initial content never loaded successfully to avoid accidental overwrite + if (!readOnly && hasLoadedOnce) { + // Guard: if the editor/model hasn't mounted, bail out + if (!editorRef.current) { + return; + } + currData.current = editorRef.current.getValue(); + if (onSave) { + await onSave(prevData.current, currData.current); + // If save succeeds, align prev with current + prevData.current = currData.current; + } } onClose(); } catch (err) { @@ -156,30 +355,30 @@ export const EditorViewer = (props: Props) => { } }); - const editorResize = useMemo( - () => - debounce(() => { - editorRef.current?.layout(); - setTimeout(() => editorRef.current?.layout(), 500); - }, 100), - [], - ); - useEffect(() => { const onResized = debounce(() => { - editorResize(); - appWindow.isMaximized().then((maximized) => { - setIsMaximized(() => maximized); - }); + appWindow + .isMaximized() + .then((maximized) => setIsMaximized(() => maximized)); + // Ensure Monaco recalculates layout after window resize/maximize/restore. + // automaticLayout is not always sufficient when the parent dialog resizes. + try { + editorRef.current?.layout(); + } catch {} }, 100); const unlistenResized = appWindow.onResized(onResized); return () => { unlistenResized.then((fn) => fn()); + // Clean up editor and model to avoid leaks + const model = editorRef.current?.getModel(); editorRef.current?.dispose(); + model?.dispose(); + modelChangeDisposableRef.current?.dispose(); + modelChangeDisposableRef.current = null; editorRef.current = undefined; }; - }, [editorResize]); + }, []); return ( @@ -188,42 +387,94 @@ export const EditorViewer = (props: Props) => { - = 1500, // 超过一定宽度显示minimap滚动条 - }, - mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例 - readOnly: readOnly, // 只读模式 - readOnlyMessage: { - value: t("profiles.modals.editor.messages.readOnly"), - }, // 只读模式尝试编辑时的提示信息 - renderValidationDecorations: "on", // 只读模式下显示校验信息 - quickSuggestions: { - strings: true, // 字符串类型的建议 - comments: true, // 注释类型的建议 - other: true, // 其他类型的建议 - }, - padding: { - top: 33, // 顶部padding防止遮挡snippets - }, - fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${ - getSystem() === "windows" ? ", twemoji mozilla" : "" - }`, - fontLigatures: false, // 连字符 - smoothScrolling: true, // 平滑滚动 - }} - beforeMount={beforeMount} - onMount={onMount} - onChange={handleChange} - /> +
+ {/* Show overlay while loading or until modelPath is ready */} + + {/* Background refresh failure helper */} + {!!refreshFailed && ( +
+ + {t("shared.feedback.notifications.common.refreshFailed")} + + +
+ )} + {initialText !== null && modelPath && ( + = 1500, + }, + mouseWheelZoom: true, + readOnly: effectiveReadOnly, + readOnlyMessage: { + value: t("profiles.modals.editor.messages.readOnly"), + }, + renderValidationDecorations: "on", + quickSuggestions: { + strings: true, + comments: true, + other: true, + }, + padding: { + top: 33, // Top padding to prevent snippet overlap + }, + fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${ + getSystem() === "windows" ? ", twemoji mozilla" : "" + }`, + fontLigatures: false, + smoothScrolling: true, + }} + beforeMount={beforeMount} + onMount={onMount} + onChange={handleChange} + /> + )} +
(props: Props) => { color="inherit" sx={{ display: readOnly ? "none" : "" }} title={t("profiles.modals.editor.actions.format")} + disabled={isLoading} onClick={() => editorRef.current ?.getAction("editor.action.formatDocument") @@ -248,7 +500,21 @@ export const EditorViewer = (props: Props) => { title={t( isMaximized ? "shared.window.minimize" : "shared.window.maximize", )} - onClick={() => appWindow.toggleMaximize().then(editorResize)} + onClick={() => + appWindow + .toggleMaximize() + .then(() => + appWindow + .isMaximized() + .then((maximized) => setIsMaximized(maximized)), + ) + .finally(() => { + // Nudge a layout in case the resize event batching lags behind + try { + editorRef.current?.layout(); + } catch {} + }) + } > {isMaximized ? : } @@ -260,7 +526,11 @@ export const EditorViewer = (props: Props) => { {t(readOnly ? "shared.actions.close" : "shared.actions.cancel")} {!readOnly && ( - )} diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index 7f928336d..eb5c68388 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -830,7 +830,8 @@ export const ProfileItem = (props: Props) => { {fileOpen && ( readProfileFile(uid)} + dataKey={uid} language="yaml" schema="clash" onSave={async (prev, curr) => { @@ -876,9 +877,10 @@ export const ProfileItem = (props: Props) => { {mergeOpen && ( readProfileFile(option?.merge ?? "")} + dataKey={`merge:${option?.merge ?? ""}`} language="yaml" - schema="clash" + schema="merge" onSave={async (prev, curr) => { await saveProfileFile(option?.merge ?? "", curr ?? ""); onSave?.(prev, curr); @@ -889,7 +891,8 @@ export const ProfileItem = (props: Props) => { {scriptOpen && ( readProfileFile(option?.script ?? "")} + dataKey={`script:${option?.script ?? ""}`} language="javascript" onSave={async (prev, curr) => { await saveProfileFile(option?.script ?? "", curr ?? ""); diff --git a/src/components/profile/profile-more.tsx b/src/components/profile/profile-more.tsx index bcc7acd34..896d54e67 100644 --- a/src/components/profile/profile-more.tsx +++ b/src/components/profile/profile-more.tsx @@ -181,9 +181,10 @@ export const ProfileMore = (props: Props) => { readProfileFile(id)} + dataKey={id} language={id === "Merge" ? "yaml" : "javascript"} - schema={id === "Merge" ? "clash" : undefined} + schema={id === "Merge" ? "merge" : undefined} onSave={async (prev, curr) => { await saveProfileFile(id, curr ?? ""); onSave?.(prev, curr); diff --git a/src/components/setting/mods/config-viewer.tsx b/src/components/setting/mods/config-viewer.tsx index 564a56dcc..d10ed2001 100644 --- a/src/components/setting/mods/config-viewer.tsx +++ b/src/components/setting/mods/config-viewer.tsx @@ -31,7 +31,8 @@ export const ConfigViewer = forwardRef((_, ref) => { } - initialData={Promise.resolve(runtimeConfig)} + initialData={() => Promise.resolve(runtimeConfig)} + dataKey="runtime-config" readOnly language="yaml" schema="clash" diff --git a/src/components/setting/mods/sysproxy-viewer.tsx b/src/components/setting/mods/sysproxy-viewer.tsx index 3d2b0f37a..5208c42b0 100644 --- a/src/components/setting/mods/sysproxy-viewer.tsx +++ b/src/components/setting/mods/sysproxy-viewer.tsx @@ -617,7 +617,8 @@ export const SysproxyViewer = forwardRef((props, ref) => { Promise.resolve(value.pac_content ?? "")} + dataKey="sysproxy-pac" language="javascript" onSave={(_prev, curr) => { let pac = DEFAULT_PAC; diff --git a/src/components/setting/mods/theme-viewer.tsx b/src/components/setting/mods/theme-viewer.tsx index b9f52a800..0f07f4ccc 100644 --- a/src/components/setting/mods/theme-viewer.tsx +++ b/src/components/setting/mods/theme-viewer.tsx @@ -9,7 +9,14 @@ import { useTheme, } from "@mui/material"; import { useLockFn } from "ahooks"; -import { useImperativeHandle, useMemo, useState } from "react"; +import { + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, + useCallback, +} from "react"; import { useTranslation } from "react-i18next"; import { BaseDialog, DialogRef } from "@/components/base"; @@ -27,6 +34,11 @@ export function ThemeViewer(props: { ref?: React.Ref }) { const { verge, patchVerge } = useVerge(); const { theme_setting } = verge ?? {}; const [theme, setTheme] = useState(theme_setting || {}); + // Latest theme ref to avoid stale closures when saving CSS + const themeRef = useRef(theme); + useEffect(() => { + themeRef.current = theme; + }, [theme]); useImperativeHandle(ref, () => ({ open: () => { @@ -55,7 +67,6 @@ export function ThemeViewer(props: { ref?: React.Ref }) { } }); - // default theme const { palette } = useTheme(); const dt = palette.mode === "light" ? defaultTheme : defaultDarkTheme; @@ -100,6 +111,13 @@ export function ThemeViewer(props: { ref?: React.Ref }) { [], ); + // Stable loader that returns a fresh Promise each call so EditorViewer + // can retry/refresh and always read the latest staged CSS from state. + const loadCss = useCallback( + () => Promise.resolve(themeRef.current?.css_injection ?? ""), + [], + ); + const renderItem = (labelKey: string, key: ThemeKey) => { const label = t(labelKey); return ( @@ -159,11 +177,15 @@ export function ThemeViewer(props: { ref?: React.Ref }) { { - theme.css_injection = curr; - handleChange("css_injection"); + onSave={async (_prev, curr) => { + // Only stage the CSS change locally. Persistence happens + // when the outer Theme dialog's Save button is pressed. + const prevTheme = themeRef.current || {}; + const nextCss = curr ?? ""; + setTheme({ ...prevTheme, css_injection: nextCss }); }} onClose={() => { setEditorOpen(false); diff --git a/src/locales/ar/shared.json b/src/locales/ar/shared.json index 9c4d4489b..5ef8d4f1f 100644 --- a/src/locales/ar/shared.json +++ b/src/locales/ar/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "تم النسخ بنجاح", "saveSuccess": "Configuration saved successfully", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "فشل التحديث" } }, "validation": { diff --git a/src/locales/de/shared.json b/src/locales/de/shared.json index eb2e34d7d..69faa3a6e 100644 --- a/src/locales/de/shared.json +++ b/src/locales/de/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "Kopieren erfolgreich", "saveSuccess": "Zufalls-Konfiguration erfolgreich gespeichert", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "Aktualisierung fehlgeschlagen" } }, "validation": { diff --git a/src/locales/en/shared.json b/src/locales/en/shared.json index 1e2f61317..e27052384 100644 --- a/src/locales/en/shared.json +++ b/src/locales/en/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "Copy Success", "saveSuccess": "Configuration saved successfully", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "Refresh failed; showing last loaded value" } }, "validation": { diff --git a/src/locales/es/shared.json b/src/locales/es/shared.json index 7ed7494c6..7babab4ef 100644 --- a/src/locales/es/shared.json +++ b/src/locales/es/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "Copia exitosa", "saveSuccess": "Configuración aleatoria guardada correctamente", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "Error al actualizar" } }, "validation": { diff --git a/src/locales/fa/shared.json b/src/locales/fa/shared.json index 6ab4e72d0..458405075 100644 --- a/src/locales/fa/shared.json +++ b/src/locales/fa/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "کپی با موفقیت انجام شد", "saveSuccess": "Configuration saved successfully", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "به‌روزرسانی ناموفق بود" } }, "validation": { diff --git a/src/locales/id/shared.json b/src/locales/id/shared.json index 057e9f74b..0effab311 100644 --- a/src/locales/id/shared.json +++ b/src/locales/id/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "Salin Berhasil", "saveSuccess": "Configuration saved successfully", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "Penyegaran gagal" } }, "validation": { diff --git a/src/locales/jp/shared.json b/src/locales/jp/shared.json index 292e7c1b2..20a88547c 100644 --- a/src/locales/jp/shared.json +++ b/src/locales/jp/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "コピー成功", "saveSuccess": "ランダム設定を保存完了", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "更新に失敗しました" } }, "validation": { diff --git a/src/locales/ko/shared.json b/src/locales/ko/shared.json index 4309e5f9c..13877c0e1 100644 --- a/src/locales/ko/shared.json +++ b/src/locales/ko/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "복사 성공", "saveSuccess": "설정이 저장되었습니다", - "saveFailed": "설정 저장 실패" + "saveFailed": "설정 저장 실패", + "refreshFailed": "새로고침 실패" } }, "validation": { diff --git a/src/locales/ru/shared.json b/src/locales/ru/shared.json index be79ca589..573de6d2f 100644 --- a/src/locales/ru/shared.json +++ b/src/locales/ru/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "Скопировано", "saveSuccess": "Configuration saved successfully", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "Не удалось обновить" } }, "validation": { diff --git a/src/locales/tr/shared.json b/src/locales/tr/shared.json index e094bdbc2..eeac37355 100644 --- a/src/locales/tr/shared.json +++ b/src/locales/tr/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "Kopyalama Başarılı", "saveSuccess": "Configuration saved successfully", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "Yenileme başarısız" } }, "validation": { diff --git a/src/locales/tt/shared.json b/src/locales/tt/shared.json index 9405b43ac..eb6316b33 100644 --- a/src/locales/tt/shared.json +++ b/src/locales/tt/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "Күчерелде", "saveSuccess": "Configuration saved successfully", - "saveFailed": "Failed to save configuration" + "saveFailed": "Failed to save configuration", + "refreshFailed": "Refresh failed" } }, "validation": { diff --git a/src/locales/zh/shared.json b/src/locales/zh/shared.json index 7db791f85..047ce8186 100644 --- a/src/locales/zh/shared.json +++ b/src/locales/zh/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "复制成功", "saveSuccess": "配置保存成功", - "saveFailed": "配置保存失败" + "saveFailed": "配置保存失败", + "refreshFailed": "刷新失败" } }, "validation": { diff --git a/src/locales/zhtw/shared.json b/src/locales/zhtw/shared.json index c7d5fe72c..089591055 100644 --- a/src/locales/zhtw/shared.json +++ b/src/locales/zhtw/shared.json @@ -89,7 +89,8 @@ "common": { "copySuccess": "複製成功", "saveSuccess": "設定儲存完成", - "saveFailed": "設定儲存失敗" + "saveFailed": "設定儲存失敗", + "refreshFailed": "重整失敗" } }, "validation": { diff --git a/src/types/generated/i18n-keys.ts b/src/types/generated/i18n-keys.ts index ed28c0e8a..5f1c376b1 100644 --- a/src/types/generated/i18n-keys.ts +++ b/src/types/generated/i18n-keys.ts @@ -706,6 +706,7 @@ export const translationKeys = [ "shared.feedback.notifications.common.copySuccess", "shared.feedback.notifications.common.saveSuccess", "shared.feedback.notifications.common.saveFailed", + "shared.feedback.notifications.common.refreshFailed", "shared.feedback.validation.config.failed", "shared.feedback.validation.config.bootFailed", "shared.feedback.validation.config.coreChangeFailed", diff --git a/src/types/generated/i18n-resources.ts b/src/types/generated/i18n-resources.ts index e64659cea..225338e1e 100644 --- a/src/types/generated/i18n-resources.ts +++ b/src/types/generated/i18n-resources.ts @@ -1200,6 +1200,7 @@ export interface TranslationResources { notifications: { common: { copySuccess: string; + refreshFailed: string; saveFailed: string; saveSuccess: string; };