From 7aff06445eed54951f4f75b1d76c482ee5dbb277 Mon Sep 17 00:00:00 2001 From: Sergey Date: Sun, 25 Oct 2026 04:55:26 +0300 Subject: [PATCH] feat(i18n): add Russian locale (#668) Add Russian beside Polish for the web app, the mobile editor, memo templates, and AI prompt seeds. Untranslated strings stay in English. iOS, the browser extension, and the marketing site are unchanged. --- .../src/components/LocalTiptapEditor.tsx | 12 +- apps/mobile/src/components/MobileMermaid.tsx | 2 +- apps/mobile/src/hooks/useMobileRichEditor.ts | 2 +- apps/mobile/src/hooks/useMobileSelectionAi.ts | 2 +- apps/mobile/src/lib/login-error.ts | 2 +- apps/mobile/src/lib/mobile-locale.test.mjs | 3 +- apps/mobile/src/lib/mobile-locale.tsx | 22 +- apps/mobile/src/lib/mobile-web-clip.ts | 10 +- apps/mobile/src/lib/preferences.ts | 4 +- apps/mobile/src/screens/WorkspaceEditors.tsx | 2 +- .../src/screens/WorkspaceMemoDetail.tsx | 2 +- .../src/screens/WorkspaceSettingsView.tsx | 1 + .../src/screens/workspace-utils.test.ts | 4 + apps/mobile/src/screens/workspace-utils.ts | 1 + apps/web/src/components/ReadOnlyX6Diagram.tsx | 7 +- apps/web/src/i18n/index.test.mjs | 8 +- apps/web/src/i18n/index.ts | 8 +- apps/web/src/i18n/locales.test.mjs | 5 + apps/web/src/i18n/locales.ts | 1 + apps/web/src/i18n/resources/ru.ts | 1 + apps/web/vite.config.ts | 9 +- packages/shared/package.json | 1 + packages/shared/src/ai-assistant.ts | 1 + packages/shared/src/ai-prompt-seeds.ts | 42 +- .../shared/src/default-workspace-seeds.ts | 17 +- packages/shared/src/i18n/catalog.test.mjs | 5 +- packages/shared/src/i18n/index.ts | 1 + packages/shared/src/i18n/locales.test.mjs | 3 + packages/shared/src/i18n/locales.ts | 6 +- packages/shared/src/i18n/ru.ts | 3264 +++++++++++++++++ packages/shared/src/memo-template-seeds.ts | 30 +- .../src/mobile-content-compatibility.ts | 7 +- packages/shared/src/mobile-editor.ts | 25 +- packages/shared/src/note-image-card.test.mjs | 3 +- scripts/verify-web-performance.mjs | 2 +- 35 files changed, 3473 insertions(+), 42 deletions(-) create mode 100644 apps/web/src/i18n/resources/ru.ts create mode 100644 packages/shared/src/i18n/ru.ts diff --git a/apps/mobile/src/components/LocalTiptapEditor.tsx b/apps/mobile/src/components/LocalTiptapEditor.tsx index e5f33b17..0cce813a 100644 --- a/apps/mobile/src/components/LocalTiptapEditor.tsx +++ b/apps/mobile/src/components/LocalTiptapEditor.tsx @@ -150,7 +150,7 @@ type LocalTiptapEditorSharedProps = { onReaderScroll?: (collapsed: boolean) => Promise; onImageExportEvent?: (payloadJson: string) => Promise; ref: Ref; - locale: "zh-CN" | "en-US" | "ja" | "pl"; + locale: "zh-CN" | "en-US" | "ja" | "pl" | "ru"; theme: "light" | "dark"; /** Live-switchable. The same DomWebView stays mounted across viewer → editor. */ mode?: "editor" | "viewer"; @@ -453,7 +453,7 @@ const ReadOnlyX6Diagram = ({ theme, }: { diagram: DiagramDocument; - locale: "zh-CN" | "en-US" | "ja" | "pl"; + locale: "zh-CN" | "en-US" | "ja" | "pl" | "ru"; theme: "light" | "dark"; }) => { const containerRef = useRef(null); @@ -1828,7 +1828,7 @@ const MobileSelectionAiPanel = ({ panel, prompts, }: { - locale: "zh-CN" | "en-US" | "ja" | "pl"; + locale: "zh-CN" | "en-US" | "ja" | "pl" | "ru"; onApply: (mode: "append" | "replace") => void; onChange: Dispatch>; onClose: () => void; @@ -2341,7 +2341,7 @@ const loadMermaid = () => { }; const createMobileCodeBlockExtension = ( - locale: "zh-CN" | "en-US" | "ja" | "pl", + locale: "zh-CN" | "en-US" | "ja" | "pl" | "ru", theme: "light" | "dark", hideCopyForVisualDiagram = false, ) => CodeBlock.extend({ @@ -2517,7 +2517,7 @@ const createMobileCodeBlockExtension = ( }); const createMobileImageSizeControls = ( - locale: "zh-CN" | "en-US" | "ja" | "pl", + locale: "zh-CN" | "en-US" | "ja" | "pl" | "ru", updateWidth: (width: number) => void ) => { const controls = document.createElement("div"); @@ -2569,7 +2569,7 @@ const createMobileImageSizeControls = ( const createProtectedImageExtension = ( baseUrl: string, - locale: "zh-CN" | "en-US" | "ja" | "pl", + locale: "zh-CN" | "en-US" | "ja" | "pl" | "ru", loadResource: (source: string) => Promise, options?: { readOnly?: boolean | (() => boolean); diff --git a/apps/mobile/src/components/MobileMermaid.tsx b/apps/mobile/src/components/MobileMermaid.tsx index 4513722c..931450c8 100644 --- a/apps/mobile/src/components/MobileMermaid.tsx +++ b/apps/mobile/src/components/MobileMermaid.tsx @@ -84,7 +84,7 @@ export const MobileMermaidDiagram = ({ source, theme, }: { - locale: "zh-CN" | "en-US" | "ja" | "pl"; + locale: "zh-CN" | "en-US" | "ja" | "pl" | "ru"; source: string; theme: "light" | "dark"; }) => { diff --git a/apps/mobile/src/hooks/useMobileRichEditor.ts b/apps/mobile/src/hooks/useMobileRichEditor.ts index 8a4cef20..32f71735 100644 --- a/apps/mobile/src/hooks/useMobileRichEditor.ts +++ b/apps/mobile/src/hooks/useMobileRichEditor.ts @@ -59,7 +59,7 @@ const createLocalImagePreviewDataUrl = async (asset: { mimeType?: string | null; }; const alertProtectedImageLoadFailure = ( - locale: "zh-CN" | "en-US" | "ja" | "pl", + locale: "zh-CN" | "en-US" | "ja" | "pl" | "ru", failure: ProtectedResourceLoadFailure ) => { const statusLabel = failure.status != null diff --git a/apps/mobile/src/hooks/useMobileSelectionAi.ts b/apps/mobile/src/hooks/useMobileSelectionAi.ts index a8542f0e..76bfad26 100644 --- a/apps/mobile/src/hooks/useMobileSelectionAi.ts +++ b/apps/mobile/src/hooks/useMobileSelectionAi.ts @@ -16,7 +16,7 @@ export const useMobileSelectionAi = ({ }: { client: MobileAiClient | null | undefined; editorRef: RefObject; - resolvedLocale: "zh-CN" | "en-US" | "ja" | "pl"; + resolvedLocale: "zh-CN" | "en-US" | "ja" | "pl" | "ru"; titleRef: RefObject; }) => { const activeRequestRef = useRef<{ requestId: string; controller: AbortController } | null>(null); diff --git a/apps/mobile/src/lib/login-error.ts b/apps/mobile/src/lib/login-error.ts index ecc47824..2c890ad8 100644 --- a/apps/mobile/src/lib/login-error.ts +++ b/apps/mobile/src/lib/login-error.ts @@ -1,7 +1,7 @@ import { ApiRequestError } from "@edgeever/client"; import { translateMobileText } from "./mobile-locale"; -export type MobileLoginLocale = "zh-CN" | "en-US" | "ja" | "pl"; +export type MobileLoginLocale = "zh-CN" | "en-US" | "ja" | "pl" | "ru"; export const MISSING_MOBILE_SESSION_MESSAGE = "登录成功但服务端没有返回移动端会话。请确认服务端已更新到支持 App 登录的版本。"; diff --git a/apps/mobile/src/lib/mobile-locale.test.mjs b/apps/mobile/src/lib/mobile-locale.test.mjs index 0e64beaf..0a007a96 100644 --- a/apps/mobile/src/lib/mobile-locale.test.mjs +++ b/apps/mobile/src/lib/mobile-locale.test.mjs @@ -29,7 +29,7 @@ describe("mobile locale translation", () => { ]; // Japanese writes "unsaved" with the same characters as Chinese. const sameInJapanese = new Set(["未保存"]); - for (const locale of ["en-US", "ja", "pl"]) { + for (const locale of ["en-US", "ja", "pl", "ru"]) { const untranslated = copy.filter((value) => translateMobileText(value, locale) === value && !(locale === "ja" && sameInJapanese.has(value))); expect({ locale, untranslated }).toEqual({ locale, untranslated: [] }); @@ -100,6 +100,7 @@ describe("mobile locale translation", () => { expect(localizeMissingNotebookName("en-US")).toBe("Uncategorized"); expect(localizeMissingNotebookName("ja")).toBe("未分類"); expect(localizeMissingNotebookName("pl")).toBe("Nieprzypisane"); + expect(localizeMissingNotebookName("ru")).toBe("Без блокнота"); expect(localizeMissingNotebookName("zh-CN")).toBe("未分类"); }); diff --git a/apps/mobile/src/lib/mobile-locale.tsx b/apps/mobile/src/lib/mobile-locale.tsx index f0e15250..2f45fd56 100644 --- a/apps/mobile/src/lib/mobile-locale.tsx +++ b/apps/mobile/src/lib/mobile-locale.tsx @@ -1,5 +1,5 @@ import { DEFAULT_MEMO_TITLE, getMemoSaveErrorReasonKey } from "@edgeever/shared"; -import { enUS, ja, pl, zhCN } from "@edgeever/shared/i18n"; +import { enUS, ja, pl, ru, zhCN } from "@edgeever/shared/i18n"; import { resolveSupportedLocale } from "@edgeever/shared/i18n/locales"; import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react"; import { @@ -8,7 +8,7 @@ import { type MobileLocalePreference, } from "./preferences"; -type SupportedMobileLocale = "zh-CN" | "en-US" | "ja" | "pl"; +type SupportedMobileLocale = "zh-CN" | "en-US" | "ja" | "pl" | "ru"; type MobileLocaleContextValue = { preference: MobileLocalePreference; resolvedLocale: SupportedMobileLocale; @@ -753,6 +753,7 @@ const zhStrings = flattenStrings(zhCN); const enStrings = flattenStrings(enUS); const jaStrings = flattenStrings(ja); const plStrings = flattenStrings(pl); +const ruStrings = flattenStrings(ru); const buildTranslationPairs = (targets: Map) => Array.from(zhStrings.entries()).flatMap(([key, source]) => { @@ -766,6 +767,7 @@ const buildTranslationPairs = (targets: Map) => const enTranslationPairs = buildTranslationPairs(enStrings); const jaTranslationPairs = buildTranslationPairs(jaStrings); const plTranslationPairs = buildTranslationPairs(plStrings); +const ruTranslationPairs = buildTranslationPairs(ruStrings); const exactEnglishTranslations = new Map( enTranslationPairs.filter((pair) => !pair.pattern).map((pair) => [pair.source, pair.target]), ); @@ -775,6 +777,9 @@ const exactJapaneseTranslations = new Map( const exactPolishTranslations = new Map( plTranslationPairs.filter((pair) => !pair.pattern).map((pair) => [pair.source, pair.target]), ); +const exactRussianTranslations = new Map( + ruTranslationPairs.filter((pair) => !pair.pattern).map((pair) => [pair.source, pair.target]), +); const mobileTemplateTranslations: TranslationPair[] = Array.from(mobileOnlyTranslations.entries()) .filter(([source]) => source.includes("{{")) .map(([source, target]) => createTranslationPair(source, target)); @@ -796,6 +801,9 @@ const polishTemplateTranslations = [ ...mobilePolishTemplateTranslations, ...plTranslationPairs.filter((pair) => pair.pattern), ].sort((left, right) => right.source.length - left.source.length); +const russianTemplateTranslations = [ + ...ruTranslationPairs.filter((pair) => pair.pattern), +].sort((left, right) => right.source.length - left.source.length); const resolveSystemLocale = (): SupportedMobileLocale => resolveSupportedLocale(Intl.DateTimeFormat().resolvedOptions().locale); @@ -828,6 +836,15 @@ export const translateMobileText = (value: string, locale: SupportedMobileLocale if (exact) return exact; return applyTemplateTranslations(value, polishTemplateTranslations) ?? value; } + if (locale === "ru") { + const exact = exactRussianTranslations.get(value); + if (exact) return exact; + const fromCatalog = applyTemplateTranslations(value, russianTemplateTranslations); + if (fromCatalog) return fromCatalog; + const english = mobileOnlyTranslations.get(value) ?? exactEnglishTranslations.get(value); + if (english) return english; + return applyTemplateTranslations(value, englishTemplateTranslations) ?? value; + } const exact = mobileOnlyTranslations.get(value) ?? exactEnglishTranslations.get(value); if (exact) { return exact; @@ -859,6 +876,7 @@ export const localizeMissingNotebookName = (locale: SupportedMobileLocale) => { if (locale === "en-US") return "Uncategorized"; if (locale === "ja") return "未分類"; if (locale === "pl") return "Nieprzypisane"; + if (locale === "ru") return "Без блокнота"; return "未分类"; }; diff --git a/apps/mobile/src/lib/mobile-web-clip.ts b/apps/mobile/src/lib/mobile-web-clip.ts index 1416ad18..bec305a1 100644 --- a/apps/mobile/src/lib/mobile-web-clip.ts +++ b/apps/mobile/src/lib/mobile-web-clip.ts @@ -27,7 +27,7 @@ export type MobileRenderedWebPage = { title?: string; }; -export type MobileWebClipLocale = "zh-CN" | "en-US" | "ja" | "pl"; +export type MobileWebClipLocale = "zh-CN" | "en-US" | "ja" | "pl" | "ru"; const webClipCopy: Record"'))]+/i; diff --git a/apps/mobile/src/lib/preferences.ts b/apps/mobile/src/lib/preferences.ts index 8b554ae9..8a132a8c 100644 --- a/apps/mobile/src/lib/preferences.ts +++ b/apps/mobile/src/lib/preferences.ts @@ -14,7 +14,7 @@ const LOCALE_PREFERENCE_KEY = "edgeever.mobile.localePreference"; const THEME_PREFERENCE_KEY = "edgeever.mobile.themePreference"; export type MobileMemoListDensity = "preview" | "compact"; -export type MobileLocalePreference = "system" | "zh-CN" | "en-US" | "ja" | "pl"; +export type MobileLocalePreference = "system" | "zh-CN" | "en-US" | "ja" | "pl" | "ru"; export type MobileThemePreference = "system" | "light" | "dark"; export const readMobileMemoListDensity = async (): Promise => { @@ -70,7 +70,7 @@ export const readMobileThemePreference = async (): Promise AsyncStorage.setItem(THEME_PREFERENCE_KEY, theme); const isMobileLocalePreference = (value: unknown): value is MobileLocalePreference => - value === "system" || value === "zh-CN" || value === "en-US" || value === "ja" || value === "pl"; + value === "system" || value === "zh-CN" || value === "en-US" || value === "ja" || value === "pl" || value === "ru"; export const readMobileAiAssistantLastAction = async ( scope: AiAssistantLastActionScope, diff --git a/apps/mobile/src/screens/WorkspaceEditors.tsx b/apps/mobile/src/screens/WorkspaceEditors.tsx index a4d5a9e2..9af98dca 100644 --- a/apps/mobile/src/screens/WorkspaceEditors.tsx +++ b/apps/mobile/src/screens/WorkspaceEditors.tsx @@ -123,7 +123,7 @@ const resolveEditableMemoTitle = (title?: string | null) => { }; const alertProtectedImageLoadFailure = ( - locale: "zh-CN" | "en-US" | "ja" | "pl", + locale: "zh-CN" | "en-US" | "ja" | "pl" | "ru", failure: ProtectedResourceLoadFailure ) => { const statusLabel = failure.status != null diff --git a/apps/mobile/src/screens/WorkspaceMemoDetail.tsx b/apps/mobile/src/screens/WorkspaceMemoDetail.tsx index fb7feefd..8a2c3789 100644 --- a/apps/mobile/src/screens/WorkspaceMemoDetail.tsx +++ b/apps/mobile/src/screens/WorkspaceMemoDetail.tsx @@ -165,7 +165,7 @@ const loadAuthenticatedImageDataUrl = ( }; const alertProtectedImageLoadFailure = ( - locale: "zh-CN" | "en-US" | "ja" | "pl", + locale: "zh-CN" | "en-US" | "ja" | "pl" | "ru", failure: ProtectedResourceLoadFailure ) => { const statusLabel = failure.status != null ? String(failure.status) : locale !== "zh-CN" ? "network error" : "网络错误"; diff --git a/apps/mobile/src/screens/WorkspaceSettingsView.tsx b/apps/mobile/src/screens/WorkspaceSettingsView.tsx index 87886b52..de3c233e 100644 --- a/apps/mobile/src/screens/WorkspaceSettingsView.tsx +++ b/apps/mobile/src/screens/WorkspaceSettingsView.tsx @@ -53,6 +53,7 @@ const MOBILE_LOCALE_OPTIONS: Array<{ label: string; value: MobileLocalePreferenc { label: "English", value: "en-US" }, { label: "日本語", value: "ja" }, { label: "Polski", value: "pl" }, + { label: "Русский", value: "ru" }, ]; type SettingsTab = "general" | "account" | "system"; export type MobileLocaleMode = MobileLocalePreference; diff --git a/apps/mobile/src/screens/workspace-utils.test.ts b/apps/mobile/src/screens/workspace-utils.test.ts index 5940ffc3..049c7506 100644 --- a/apps/mobile/src/screens/workspace-utils.test.ts +++ b/apps/mobile/src/screens/workspace-utils.test.ts @@ -65,6 +65,7 @@ describe("mobile workspace utilities", () => { expect(getResolvedMobileLocale("zh-CN")).toBe("zh-CN"); expect(getResolvedMobileLocale("en-US")).toBe("en-US"); expect(getResolvedMobileLocale("pl")).toBe("pl"); + expect(getResolvedMobileLocale("ru")).toBe("ru"); expect(isEnglishMobileLocale("zh-CN")).toBe(false); expect(isEnglishMobileLocale("en-US")).toBe(true); }); @@ -85,6 +86,9 @@ describe("mobile workspace utilities", () => { expect(isEnglishMobileLocale("system")).toBe(true); withLocale("pl"); expect(getResolvedMobileLocale("system")).toBe("pl"); + withLocale("ru-RU"); + expect(getResolvedMobileLocale("system")).toBe("ru"); + expect(isEnglishMobileLocale("system")).toBe(true); withLocale("fr-FR"); expect(getResolvedMobileLocale("system")).toBe("en-US"); expect(isEnglishMobileLocale("system")).toBe(true); diff --git a/apps/mobile/src/screens/workspace-utils.ts b/apps/mobile/src/screens/workspace-utils.ts index 59f33e60..94fd2aae 100644 --- a/apps/mobile/src/screens/workspace-utils.ts +++ b/apps/mobile/src/screens/workspace-utils.ts @@ -144,6 +144,7 @@ export const formatMemoPreviewDate = (value: string, localePreference: MobileLoc if (memoDay === today - 24 * 60 * 60 * 1000) { if (locale === "ja") return "昨日"; if (locale === "pl") return "Wczoraj"; + if (locale === "ru") return "Вчера"; if (locale === "en-US") return "Yesterday"; return "昨天"; } diff --git a/apps/web/src/components/ReadOnlyX6Diagram.tsx b/apps/web/src/components/ReadOnlyX6Diagram.tsx index 44b25655..a9b5b154 100644 --- a/apps/web/src/components/ReadOnlyX6Diagram.tsx +++ b/apps/web/src/components/ReadOnlyX6Diagram.tsx @@ -15,13 +15,16 @@ import { Graph.registerConnector(MIND_MAP_CONNECTOR_NAME, mindMapConnector, true); -const diagramTitle = (diagram: DiagramDocument, locale: "zh-CN" | "en-US" | "ja" | "pl") => { +const diagramTitle = (diagram: DiagramDocument, locale: "zh-CN" | "en-US" | "ja" | "pl" | "ru") => { if (locale === "en-US") { return diagram.kind === "mind-map" ? "Mind map" : diagram.kind === "architecture" ? "Architecture diagram" : "Flowchart"; } if (locale === "pl") { return diagram.kind === "mind-map" ? "Mapa myśli" : diagram.kind === "architecture" ? "Diagram architektury" : "Schemat blokowy"; } + if (locale === "ru") { + return diagram.kind === "mind-map" ? "Ментальная карта" : diagram.kind === "architecture" ? "Схема архитектуры" : "Блок-схема"; + } return diagram.kind === "mind-map" ? "思维导图" : diagram.kind === "architecture" ? "架构图" : "流程图"; }; @@ -32,7 +35,7 @@ export const ReadOnlyX6Diagram = ({ controls = false, }: { diagram: DiagramDocument; - locale: "zh-CN" | "en-US" | "ja" | "pl"; + locale: "zh-CN" | "en-US" | "ja" | "pl" | "ru"; theme: "light" | "dark"; controls?: boolean; }) => { diff --git a/apps/web/src/i18n/index.test.mjs b/apps/web/src/i18n/index.test.mjs index e5ab875a..8cfd66cf 100644 --- a/apps/web/src/i18n/index.test.mjs +++ b/apps/web/src/i18n/index.test.mjs @@ -23,7 +23,7 @@ describe("lazy Japanese catalog", () => { expect(viteConfig).toContain("globPatterns: []"); expect(viteConfig).toContain("name: \"i18n-ja\""); expect(viteConfig).toContain("edgeever-optional-locales"); - expect(viteConfig).toContain("i18n-(?:ja|pl)-"); + expect(viteConfig).toContain("i18n-(?:ja|pl|ru)-"); }); test("loads Polish on demand like Japanese", () => { @@ -31,4 +31,10 @@ describe("lazy Japanese catalog", () => { expect(source).toContain('await import("./resources/pl")'); expect(viteConfig).toContain("name: \"i18n-pl\""); }); + + test("loads Russian on demand like Polish", () => { + expect(source).not.toContain('import { ru } from "./resources/ru"'); + expect(source).toContain('await import("./resources/ru")'); + expect(viteConfig).toContain("name: \"i18n-ru\""); + }); }); diff --git a/apps/web/src/i18n/index.ts b/apps/web/src/i18n/index.ts index 25d67041..a78e6c02 100644 --- a/apps/web/src/i18n/index.ts +++ b/apps/web/src/i18n/index.ts @@ -30,13 +30,16 @@ export const resources = { type OptionalLocale = Exclude; const isOptionalLocale = (locale: SupportedLocale): locale is OptionalLocale => - locale === "ja" || locale === "pl"; + locale === "ja" || locale === "pl" || locale === "ru"; const loadOptionalCatalog = async (locale: OptionalLocale) => { if (locale === "ja") { return (await import("./resources/ja")).ja; } - return (await import("./resources/pl")).pl; + if (locale === "pl") { + return (await import("./resources/pl")).pl; + } + return (await import("./resources/ru")).ru; }; const ensureLocaleCatalog = async (locale: SupportedLocale) => { @@ -53,6 +56,7 @@ void i18n.use(initReactI18next).init({ fallbackLng: { ja: ["en-US"], pl: ["en-US"], + ru: ["en-US"], default: [defaultLocale], }, supportedLngs: supportedLocales, diff --git a/apps/web/src/i18n/locales.test.mjs b/apps/web/src/i18n/locales.test.mjs index 014a3d30..2107495b 100644 --- a/apps/web/src/i18n/locales.test.mjs +++ b/apps/web/src/i18n/locales.test.mjs @@ -46,6 +46,11 @@ describe("web locale resolution", () => { expect(getBrowserLocale()).toBe("pl"); }); + test("keeps Russian browser languages on the shipped ru locale", () => { + setNavigatorLanguages(["ru-RU", "en-US"]); + expect(getBrowserLocale()).toBe("ru"); + }); + test("falls unmatched browser languages back to English instead of Chinese", () => { setNavigatorLanguages(["fr-FR", "de-DE"]); expect(getBrowserLocale()).toBe("en-US"); diff --git a/apps/web/src/i18n/locales.ts b/apps/web/src/i18n/locales.ts index 985938c4..5d2f1dc8 100644 --- a/apps/web/src/i18n/locales.ts +++ b/apps/web/src/i18n/locales.ts @@ -21,6 +21,7 @@ export const localeLabels: Record = { "en-US": "English", ja: "日本語", pl: "Polski", + ru: "Русский", }; export const normalizeLocale = (locale: string | null | undefined): SupportedLocale | null => diff --git a/apps/web/src/i18n/resources/ru.ts b/apps/web/src/i18n/resources/ru.ts new file mode 100644 index 00000000..7f99b641 --- /dev/null +++ b/apps/web/src/i18n/resources/ru.ts @@ -0,0 +1 @@ +export { ru } from "@edgeever/shared/i18n/ru"; diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index a4a86017..9bb9188e 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -223,7 +223,7 @@ export default defineConfig({ }, }, { - urlPattern: ({ url }) => /\/assets\/i18n-(?:ja|pl)-/.test(url.pathname), + urlPattern: ({ url }) => /\/assets\/i18n-(?:ja|pl|ru)-/.test(url.pathname), handler: "CacheFirst", options: { cacheName: "edgeever-optional-locales", @@ -292,7 +292,7 @@ export default defineConfig({ ? false : { resolveDependencies: (_filename, dependencies) => dependencies.filter((dependency) => - !/(?:vendor-code-highlight|vendor-(?:mermaid|D3|tiptap|prosemirror|floating|codemirror|x6|infographic|zod|streamdown)|vendor~(?:wasm|emacs-lisp)-|vendor-radix(?!-slot)|ui-primitives|ui-button-tooltip|i18n-(?:ja|pl)-)/.test(dependency), + !/(?:vendor-code-highlight|vendor-(?:mermaid|D3|tiptap|prosemirror|floating|codemirror|x6|infographic|zod|streamdown)|vendor~(?:wasm|emacs-lisp)-|vendor-radix(?!-slot)|ui-primitives|ui-button-tooltip|i18n-(?:ja|pl|ru)-)/.test(dependency), ), }, rolldownOptions: { @@ -347,6 +347,11 @@ export default defineConfig({ test: /[\\/]i18n[\\/](?:resources[\\/])?pl\.ts$/, priority: 41, }, + { + name: "i18n-ru", + test: /[\\/]i18n[\\/](?:resources[\\/])?ru\.ts$/, + priority: 41, + }, { name: "vendor-react", test: /node_modules[\\/](react|react-dom|scheduler|react-router)[\\/]/, diff --git a/packages/shared/package.json b/packages/shared/package.json index b10135a5..b446d94b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -20,6 +20,7 @@ "./i18n/zh-CN": "./src/i18n/zh-CN.ts", "./i18n/ja": "./src/i18n/ja.ts", "./i18n/pl": "./src/i18n/pl.ts", + "./i18n/ru": "./src/i18n/ru.ts", "./i18n/locales": "./src/i18n/locales.ts", "./memo-template-seeds": "./src/memo-template-seeds.ts", "./default-workspace-seeds": "./src/default-workspace-seeds.ts", diff --git a/packages/shared/src/ai-assistant.ts b/packages/shared/src/ai-assistant.ts index 5f66f8dc..f7a76bbc 100644 --- a/packages/shared/src/ai-assistant.ts +++ b/packages/shared/src/ai-assistant.ts @@ -111,6 +111,7 @@ export const getDefaultAiTargetLanguage = (locale: string | undefined): AiTarget if (normalized.startsWith("zh")) return "en"; if (normalized.startsWith("ja")) return "ja"; if (normalized.startsWith("pl")) return "en"; + if (normalized.startsWith("ru")) return "en"; return "zh-CN"; }; diff --git a/packages/shared/src/ai-prompt-seeds.ts b/packages/shared/src/ai-prompt-seeds.ts index a7f770a4..74f08ebd 100644 --- a/packages/shared/src/ai-prompt-seeds.ts +++ b/packages/shared/src/ai-prompt-seeds.ts @@ -6,7 +6,7 @@ import type { import { resolveSupportedLocale } from "./i18n/locales"; export type AiPromptSeedKey = Exclude; -export type AiPromptSeedLocale = "zh-CN" | "en-US" | "ja" | "pl"; +export type AiPromptSeedLocale = "zh-CN" | "en-US" | "ja" | "pl" | "ru"; export type AiPromptSeedTranslation = { name: string; @@ -29,10 +29,11 @@ const seed = ( enUS: AiPromptSeedTranslation, ja: AiPromptSeedTranslation, pl: AiPromptSeedTranslation, + ru: AiPromptSeedTranslation, ): AiPromptSeed => ({ ...metadata, ...zhCN, - translations: { "zh-CN": zhCN, "en-US": enUS, ja, pl }, + translations: { "zh-CN": zhCN, "en-US": enUS, ja, pl, ru }, }); export const normalizeAiPromptSeedLocale = (locale: string | null | undefined): AiPromptSeedLocale => @@ -110,6 +111,18 @@ export const DEFAULT_AI_PROMPT_SEEDS: readonly AiPromptSeed[] = [ "Zachowaj język notatki i zwróć wyłącznie podsumowanie w formacie Markdown.", ].join(""), }, + { + name: "Кратко", + description: "Сжать заметку до темы, выводов и действий", + instruction: [ + "Сделайте настоящее краткое изложение заметки, а не пересказ предложения за предложением и не повтор текста. ", + "Выделите главную тему, основные мысли, ключевые выводы и действия. ", + "Опустите повторы, риторику, примеры, цитаты и второстепенные детали, если без них вывод всё равно понятен. ", + "Для длинной заметки ориентируйтесь примерно на 20–30% исходного объёма и 3–7 коротких пунктов Markdown; для короткой хватит 1–3 предложений. ", + "Не копируйте длинные фрагменты дословно и не добавляйте факты, которых нет в тексте. ", + "Сохраните язык заметки и верните только краткое изложение в Markdown.", + ].join(""), + }, ), seed( { key: "translate", action: "translate", parameterKind: "target-language", resultMode: "both" }, @@ -133,6 +146,11 @@ export const DEFAULT_AI_PROMPT_SEEDS: readonly AiPromptSeed[] = [ description: "Przetłumacz na wybrany język, zachowując formatowanie", instruction: "Przetłumacz całą notatkę na język docelowy wskazany przez użytkownika. Zachowaj jej znaczenie, strukturę Markdown, linki i bloki kodu. Zwróć wyłącznie przetłumaczoną notatkę, bez komentarzy.", }, + { + name: "Перевести", + description: "Перевести на выбранный язык, сохранив оформление", + instruction: "Переведите заметку целиком на язык, который указал пользователь. Сохраните смысл, структуру Markdown, ссылки и блоки кода. Верните только перевод, без комментариев.", + }, ), seed( { key: "improve-writing", action: "improve-writing", parameterKind: "none", resultMode: "both" }, @@ -156,6 +174,11 @@ export const DEFAULT_AI_PROMPT_SEEDS: readonly AiPromptSeed[] = [ description: "Popraw język oraz zwiększ przejrzystość i płynność tekstu", instruction: "Dopracuj treść: popraw pisownię, gramatykę i interpunkcję oraz ulepsz dobór słów, budowę zdań, przejrzystość i płynność. Nie zmieniaj jej znaczenia ani celowo jej nie skracaj. Zachowaj jej język i przydatne formatowanie Markdown. Zwróć wyłącznie dopracowaną treść.", }, + { + name: "Улучшить текст", + description: "Исправить язык и сделать текст яснее и плавнее", + instruction: "Улучшите текст: исправьте орфографию, грамматику и пунктуацию, подберите слова, структуру фраз, ясность и плавность. Не меняйте смысл и не сокращайте текст нарочно. Сохраните язык и полезное оформление Markdown. Верните только улучшенный текст.", + }, ), seed( { key: "make-shorter", action: "make-shorter", parameterKind: "none", resultMode: "both" }, @@ -179,6 +202,11 @@ export const DEFAULT_AI_PROMPT_SEEDS: readonly AiPromptSeed[] = [ description: "Usuń powtórzenia i napisz tekst zwięźle i konkretnie", instruction: "Dopracuj treść, usuwając powtórzenia, puste frazy i zbędne określenia oraz łącząc zdania tam, gdzie to pomaga. Spraw, by tekst był zwięzły, jasny i konkretny, zachowując każdy kluczowy fakt, tezę i pierwotne znaczenie. Nie dodawaj nowych informacji. Zachowaj jej język i przydatne formatowanie Markdown. Zwróć wyłącznie dopracowaną treść.", }, + { + name: "Короче и яснее", + description: "Убрать повторы и написать короче и прямее", + instruction: "Сократите текст: уберите повторы, пустые фразы и лишние определения, объедините предложения, где это уместно. Сделайте его коротким, ясным и прямым, сохранив каждый важный факт, мысль и исходный смысл. Не добавляйте новой информации. Сохраните язык и полезное оформление Markdown. Верните только сокращённый текст.", + }, ), seed( { key: "extract-todos", action: "extract-todos", parameterKind: "none", resultMode: "append" }, @@ -202,6 +230,11 @@ export const DEFAULT_AI_PROMPT_SEEDS: readonly AiPromptSeed[] = [ description: "Znajdź działania do wykonania i utwórz listę zadań", instruction: "Wyodrębnij z notatki jawne lub domniemane zadania do wykonania jako listę zadań Markdown z użyciem '- [ ]'. Zachowaj jej język i nie wymyślaj zadań. Jeśli nie ma zadań do wykonania, napisz to krótko w języku notatki.", }, + { + name: "Выделить задачи", + description: "Найти дела и составить список задач", + instruction: "Выделите из заметки явные и подразумеваемые дела и оформите их списком задач Markdown через '- [ ]'. Сохраните язык заметки и не придумывайте задачи. Если дел нет, коротко напишите об этом на языке заметки.", + }, ), seed( { key: "continue-writing", action: "continue-writing", parameterKind: "none", resultMode: "append" }, @@ -225,6 +258,11 @@ export const DEFAULT_AI_PROMPT_SEEDS: readonly AiPromptSeed[] = [ description: "Kontynuuj naturalnie od końca notatki", instruction: "Kontynuuj pisanie naturalnie od miejsca, w którym kończy się notatka. Zwróć wyłącznie nową kontynuację, bez oryginalnej treści. Zachowaj jej język i styl Markdown.", }, + { + name: "Продолжить текст", + description: "Продолжить естественно с конца заметки", + instruction: "Продолжите текст естественно с того места, где заметка заканчивается. Верните только продолжение, без исходного текста. Сохраните язык и стиль Markdown.", + }, ), ]; diff --git a/packages/shared/src/default-workspace-seeds.ts b/packages/shared/src/default-workspace-seeds.ts index 0e687600..4ec0ef7e 100644 --- a/packages/shared/src/default-workspace-seeds.ts +++ b/packages/shared/src/default-workspace-seeds.ts @@ -16,35 +16,35 @@ export const DEFAULT_NOTEBOOK_SEEDS: readonly DefaultNotebookSeed[] = [ slug: "inbox", color: "#0f766e", sortOrder: 10, - names: { "zh-CN": "等待分类", "en-US": "Inbox", ja: "受信トレイ", pl: "Skrzynka" }, + names: { "zh-CN": "等待分类", "en-US": "Inbox", ja: "受信トレイ", pl: "Skrzynka", ru: "Входящие" }, }, { key: "projects", slug: "work-projects", color: "#2563eb", sortOrder: 20, - names: { "zh-CN": "工作项目", "en-US": "Work Projects", ja: "仕事のプロジェクト", pl: "Projekty zawodowe" }, + names: { "zh-CN": "工作项目", "en-US": "Work Projects", ja: "仕事のプロジェクト", pl: "Projekty zawodowe", ru: "Рабочие проекты" }, }, { key: "learning", slug: "learning-resources", color: "#7c3aed", sortOrder: 30, - names: { "zh-CN": "学习资料", "en-US": "Learning Resources", ja: "学習資料", pl: "Materiały do nauki" }, + names: { "zh-CN": "学习资料", "en-US": "Learning Resources", ja: "学習資料", pl: "Materiały do nauki", ru: "Учебные материалы" }, }, { key: "creative", slug: "creative-ideas", color: "#db2777", sortOrder: 40, - names: { "zh-CN": "灵感创作", "en-US": "Creative Ideas", ja: "アイデアと創作", pl: "Pomysły i twórczość" }, + names: { "zh-CN": "灵感创作", "en-US": "Creative Ideas", ja: "アイデアと創作", pl: "Pomysły i twórczość", ru: "Творческие идеи" }, }, { key: "personal", slug: "personal-life", color: "#ea580c", sortOrder: 50, - names: { "zh-CN": "生活个人", "en-US": "Personal Life", ja: "暮らしと個人", pl: "Życie prywatne" }, + names: { "zh-CN": "生活个人", "en-US": "Personal Life", ja: "暮らしと個人", pl: "Życie prywatne", ru: "Личная жизнь" }, }, ]; @@ -101,6 +101,13 @@ export const WELCOME_MEMO_TRANSLATIONS: Record { describe("shared i18n catalogs", () => { test("ships Japanese alongside Chinese and English with matching keys", () => { - expect(supportedLocales).toEqual(["zh-CN", "en-US", "ja", "pl"]); + expect(supportedLocales).toEqual(["zh-CN", "en-US", "ja", "pl", "ru"]); expect(collectKeys(ja)).toEqual(collectKeys(enUS)); expect(collectKeys(ja)).toEqual(collectKeys(zhCN)); expect(ja.login.title).toContain("サインイン"); expect(ja.editor.imageScale).toContain("画像"); expect(collectKeys(pl)).toEqual(collectKeys(enUS)); expect(pl.login.title).toContain("Zaloguj"); + expect(collectKeys(ru)).toEqual(collectKeys(enUS)); + expect(ru.login.title).toContain("Вход"); }); }); diff --git a/packages/shared/src/i18n/index.ts b/packages/shared/src/i18n/index.ts index 037a2f6e..c5d3544f 100644 --- a/packages/shared/src/i18n/index.ts +++ b/packages/shared/src/i18n/index.ts @@ -2,6 +2,7 @@ export { enUS } from "./en-US"; export { zhCN } from "./zh-CN"; export { ja } from "./ja"; export { pl } from "./pl"; +export { ru } from "./ru"; export { defaultLocale, matchSupportedLocale, diff --git a/packages/shared/src/i18n/locales.test.mjs b/packages/shared/src/i18n/locales.test.mjs index 4cdaf88d..e2b87e17 100644 --- a/packages/shared/src/i18n/locales.test.mjs +++ b/packages/shared/src/i18n/locales.test.mjs @@ -23,6 +23,9 @@ describe("supported locale matching", () => { expect(matchSupportedLocale("pl")).toBe("pl"); expect(matchSupportedLocale("pl-PL")).toBe("pl"); expect(matchSupportedLocale("pl_PL")).toBe("pl"); + expect(matchSupportedLocale("ru")).toBe("ru"); + expect(matchSupportedLocale("ru-RU")).toBe("ru"); + expect(matchSupportedLocale("ru_RU")).toBe("ru"); }); test("does not treat unrelated tags as a shipped locale", () => { diff --git a/packages/shared/src/i18n/locales.ts b/packages/shared/src/i18n/locales.ts index d9980e63..2a9f11db 100644 --- a/packages/shared/src/i18n/locales.ts +++ b/packages/shared/src/i18n/locales.ts @@ -1,4 +1,4 @@ -export const supportedLocales = ["zh-CN", "en-US", "ja", "pl"] as const; +export const supportedLocales = ["zh-CN", "en-US", "ja", "pl", "ru"] as const; export type SupportedLocale = (typeof supportedLocales)[number]; @@ -31,6 +31,10 @@ export const matchSupportedLocale = (locale: string | null | undefined): Support return "pl"; } + if (normalized === "ru" || normalized.startsWith("ru-")) { + return "ru"; + } + return null; }; diff --git a/packages/shared/src/i18n/ru.ts b/packages/shared/src/i18n/ru.ts new file mode 100644 index 00000000..68e5c854 --- /dev/null +++ b/packages/shared/src/i18n/ru.ts @@ -0,0 +1,3264 @@ +import { memoTemplateSeedTranslations } from "../memo-template-seeds"; + +// Shared by the PWA and native mobile clients. +export const ru = { + fontLoad: {"templateLoading": "Loading font…", "templateFallback": "Temporarily using substitute fonts. The original fonts will be restored automatically when loaded.", "loading": "Loading font…", "ready": "Font ready", "error": "Font could not load", "retry": "Retry"}, + noteProperties: {"heading": "Свойства заметки", "source": "Показать исходник YAML", "name": "Имя свойства", "type": "Тип свойства", "add": "Добавить свойство", "remove": "Удалить {{name}}", "invalidName": "Укажите уникальное имя, которое не занято системным свойством.", "invalidYaml": "Свойства требуют внимания:", "invalidNumber": "Введите корректное число.", "listHint": "По одному значению на строку", "text": "Текст", "list": "Список", "number": "Число", "checkbox": "Флажок", "date": "Дата"}, + poster: { + design: { + downloadSource: "Скачать исходник", + copyFailed: "Не удалось скопировать изображение. Используйте экспорт.", + canvasTab: "Холст", + "name": "Дизайн постера", + "templates": "Шаблоны", + "assets": "Материалы", + "ai": "Дизайн ИИ", + "copy": "Текст", + "headline": "Заголовок", + "subtitle": "Подзаголовок", + "badge": "Значок / подпись", + "reflow": "Обновить текст и перестроить", + "allTemplates": "Все шаблоны", + "recommended": "Рекомендуемые", + "searchTemplates": "Поиск по стилю или назначению…", + "templateCredit": "Движок вёрстки адаптирован из Qiaomu Design · лицензия MIT", + "platform": "Площадка публикации", + "customSize": "Свой размер", + "width": "Ширина", + "height": "Высота", + "gradient": "Градиент", + "solid": "Сплошной", + "gradientFrom": "Начальный цвет", + "gradientTo": "Конечный цвет", + "rotation": "Поворот", + "opacity": "Непрозрачность", + "position": "Положение и размер", + "typography": "Типографика", + "italic": "Курсив", + "underline": "Подчёркивание", + "lineHeight": "Межстрочный интервал", + "tracking": "Межбуквенный интервал", + "badgeBackground": "Фон значка", + "fill": "Заливка", + "radius": "Скругление углов", + "imageAdjust": "Настройка изображения", + "fitImage": "Вписать изображение", + "coverImage": "Заполнить холст", + "flipHorizontal": "Отразить по горизонтали", + "flipVertical": "Отразить по вертикали", + "cropX": "Положение обрезки X (%)", + "cropY": "Положение обрезки Y (%)", + "outline": "Обводка", + "strokeWidth": "Толщина", + "shadow": "Тень", + "enableShadow": "Включить тень", + "blur": "Размытие", + "shape": "Фигуры", + "images": "Изображения", + "uploadImage": "Загрузить изображение", + "searchImages": "Поиск изображений в заметках…", + "imageLibraryHint": "Изображения из других заметок копируются в этот постер.", + "layerCount": "Слоёв: {{count}}", + "selectionCount": "Выбрано элементов: {{count}}", + "show": "Показать слой", + "hide": "Скрыть слой", + "group": "Сгруппировать", + "ungroup": "Разгруппировать", + "multiSelectHint": "Щёлкните с Shift, чтобы выбрать несколько слоёв; сгруппируйте их, чтобы двигать и масштабировать вместе.", + "selectHint": "Выберите элемент на холсте или в списке слоёв, чтобы его настроить.", + "duplicate": "Дублировать элементы", + "drawing": "Рисование от руки", + "copyImage": "Копировать изображение", + "copied": "Изображение скопировано", + "export": "Экспорт изображения", + "exportScale": "Масштаб экспорта", + "exportQuality": "Качество изображения", + "download": "Скачать изображение", + "select": "Выбор элементов", + "pan": "Двигать холст (Space)", + "draw": "Рисовать", + "grid": "Сетка", + "snap": "Привязка", + "safeArea": "Безопасная зона площадки", + "canvasHint": "Двойной щелчок — правка текста · Shift — выбор · Space — двигать холст", + "zoom": "Масштаб холста", + "zoomIn": "Увеличить", + "zoomOut": "Уменьшить", + "fit": "Вписать в окно", + "aiSelection": "Править выделение", + "aiGenerate": "Сгенерировать 3 макета", + "aiSelectionHint": "Меняются только выбранные элементы ({{count}}); остальной макет сохраняется.", + "aiCanvasHint": "По вашему тексту и стилю будут сгенерированы 3 полных макета. Сначала посмотрите их, затем примените.", + "aiPlaceholder": "Например: чёрно-зелёная обложка для чтения, крупный заголовок и спокойный подзаголовок…", + "aiPreviewHint": "Для текста и макета используется настроенная модель ИИ. Посмотрите результат перед применением; потом его можно отменить.", + "aiPreview": "Выберите макет", + "aiStale": "Пока вы смотрели варианты, макет изменился. Сгенерируйте предложение заново.", + "align": { + "left": "По левому краю", + "center": "По центру горизонтально", + "right": "По правому краю", + "top": "По верхнему краю", + "middle": "По центру вертикально", + "bottom": "По нижнему краю" + }, + "textAlign": { + "left": "Текст слева", + "center": "Текст по центру", + "right": "Текст справа" + }, + "textStyles": { + "heading": "Добавить заголовок", + "subtitle": "Добавить подзаголовок", + "body": "Добавить основной текст", + "badge": "Добавить значок" + }, + "shapes": { + "rect": "Прямоугольник", + "ellipse": "Круг", + "pill": "Скруглённый прямоугольник", + "line": "Линия", + "arrow": "Стрелка", + "star": "Звезда", + "sparkle": "Искра" + } +}, + aiInvalid: "Ответ ИИ с макетом не удалось использовать. Измените запрос и повторите.", + accent: "Акцент", + offline: "Подключитесь снова, чтобы сохранить превью или добавить изображения. Черновик остаётся на этом устройстве.", + "name": "Постер", + "title": "Название постера", + "defaultHeading": "Ваш заголовок", + "defaultSubtitle": "Превратите идеи в то, чем хочется поделиться", + "createFromNote": "Создать постер", + "back": "К заметкам", + "save": "Сохранить", + "saving": "Сохранение…", + "unsaved": "Не сохранено", + "saved": "Сохранено на этом устройстве", + "format": "Размер холста", + "text": "Текст", + "rect": "Прямоугольник", + "ellipse": "Эллипс", + "image": "Добавить изображение", + "undo": "Отменить", + "redo": "Повторить", + "insertSource": "Вставить в исходную заметку", + "canvas": "Холст постера", + "templates": "Шаблоны", + "ai": "Подсказки макета ИИ", + "prompt": "Опишите тему, текст или стиль…", + "propose": "Предложить макет", + "proposal": "Проверка предложения макета", + "background": "Цвет фона", + "properties": "Свойства элемента", + "layerName": "Имя слоя", + "fontSize": "Размер шрифта", + "fontFamily": "Гарнитура", + "sans": "Гротеск", + "serif": "Антиква", + "mono": "Моноширинный", + "bold": "Полужирный", + "color": "Цвет элемента", + "center": "По центру горизонтально", + "lock": "Заблокировать", + "unlock": "Разблокировать", + "delete": "Удалить элемент", + "layers": "Слои", + "front": "На передний план", + "backLayer": "На задний план", + "hint": "Перемещайте, меняйте размер и поворачивайте элементы; дважды щёлкните текст, чтобы править. Изменения сохраняются сами.", + "readOnly": "Этот постер только для просмотра. Изображение можно экспортировать.", + "replaceTemplate": "Заменить шаблон?", + "replaceHint": "Заголовок и подзаголовок сохранятся, макет заменится. Остальные элементы будут заменены. Это можно отменить.", + "apply": "Применить макет", + "error": "Не удалось выполнить действие. Повторите попытку; черновик остаётся на этом устройстве.", + "invalid": "Исходник постера повреждён или превышает лимиты. Сохраните исходник и откройте его совместимой версией.", + "retry": "Повторить", + "recover": "Найден несохранённый черновик постера.", + "restore": "Восстановить черновик", + "downloadDraft": "Скачать черновик", + "later": "Позже", + "staleDraft": "Этот черновик сделан от другой версии заметки. Сначала скачайте его и сравните обе копии.", + "recoveryError": "Локальная копия черновика недоступна. Сохраните сейчас.", + "remoteChanged": "Постер обновлён на другом устройстве. Ваш черновик сохранён: скачайте его и сравните версии.", + "sourceUnavailable": "Исходная заметка удалена или не принимает изображения.", + "imageLimit": "Выберите PNG, JPEG или WebP до 10 МБ и 36 мегапикселей.", + "formats": { + "portrait": "Портрет 3:4", + "square": "Квадрат 1:1", + "wechat": "Обложка статьи", + "video": "Обложка видео 16:9", + "story": "История 9:16" + }, + "templatesList": { + "editorial": "Редакционный", + "bold": "Крупный заголовок", + "quote": "Карточка с цитатой", + "minimal": "Минимализм" + } +}, + infographic: { + name: "Инфографика", autoLayout: "Автоматическое размещение", describe: "Описать и сгенерировать", chooseTemplate: "Выберите шаблон и заполните содержимое", + prompt: "Опишите информацию, структуру и ключевые моменты", generate: "Создать инфографику", generating: "Генерация…", + refine: "Опишите, что нужно изменить", refinePrompt: "Например: измените второй элемент на рост пользователей", applyRefinement: "Пересмотр с помощью AI", undoGeneration: "Отменить последнее поколение", fineTune: "Точная настройка вручную", + historyTitle: "Журнал бесед", scrollToBottom: "Перейти к последнему сообщению", historyGenerated: "Сгенерировано «{{title}}»", historyRefined: "Пересмотрено «{{title}}»", historyClarified: "Необходима более подробная информация.", historyFailed: "Эта редакция не была завершена", historyUndone: "Эта редакция была отменена", + doubleClickText: "Дважды щелкните текст на графике, чтобы отредактировать его. Изменения сохраняются автоматически.", + structureSettings: "Структура и стиль · Массовое редактирование", editSyntax: "Изменить синтаксис источника", backToStructure: "Назад к структуре и стилю", + title: "Название", description: "Описание", items: "Элементы (по одному на строку, используйте «Заголовок | Описание»)", comparisonItems: "Одна строка с каждой стороны; добавьте аспекты как «- аспект | описание»", template: "Шаблон", theme: "Цветовая тема", + content: "Содержание", syntax: "Синтаксис", preview: "Просмотр", save: "Сохранить", saving: "Сохранение…", export: "Экспорт", exportSvg: "Экспорт файла SVG", exportPng: "Экспорт PNG", + exportError: "Не удалось экспортировать изображение. Повторите попытку.", + empty: "Опишите свой контент или начните с шаблона.", noPreview: "Добавьте контент, чтобы увидеть предварительный просмотр.", + invalidSyntax: "Недопустимый синтаксис инфографики. Проверьте и повторите попытку.", renderError: "Сбой визуализации. Настройте синтаксис или шаблон.", contentTooLarge: "Эта инфографика слишком велика для редактирования ИИ. Сначала сократите его.", + saveError: "Не удалось сохранить. Повторите попытку.", sessionError: "Не удалось начать сеанс редактирования. Повторите попытку.", aiError: "Сбой генерации. Проверьте настройки модели ИИ и повторите попытку.", aiInvalidResponse: "Созданному контенту не удалось создать инфографику. Повторите попытку.", + selectTemplate: "Выбрать шаблон", simpleList: "Горизонтальные шаги", numberedSteps: "Пронумерованные шаги", zigzagSteps: "Зигзагообразный процесс", verticalList: "Вертикальный список", gridList: "Сетка/список", pyramidList: "Пирамидальный список", timeline: "Срок", timelineChecklist: "Хронология этапов", roadmap: "Вертикальная дорожная карта", comparison: "Сравнение бок о бок", compactComparison: "Компактное сравнение", arrowComparison: "Сравнение направлений", quadrant: "Четыре квадранта", circularQuadrant: "Круговой квадрант", themeDefault: "По умолчанию", themeDark: "Тёмная", + stepsGroup: "Технологическая часть", listGroup: "Перечень", timelineGroup: "Время и дорожная карта", comparisonGroup: "Сравнение", quadrantGroup: "Четыре квадранта", + officialTemplates: "Шаблоны AntV ({{count}})", searchTemplates: "Шаблон поиска", templateCategory: "Категория шаблона", chooseCategory: "Какой тип инфографики подходит для этого контента?", keepCategory: "Сохранить текущий тип", allCategories: "Все рубрики", chartCategory: "График", comparisonCategory: "Сравнение", hierarchyCategory: "Иерархия", listCategory: "Перечень", quadrantCategory: "Четыре квадранта", relationCategory: "Связи", sequenceCategory: "Последовательность", + galleryTitle: "Примеры и шаблоны инфографики", gallerySubtitle: "Готовые структуры и макеты. Нажмите пример, чтобы загрузить и править его, или опишите задачу ИИ справа.", + galleryEmptyHint: "Холст пуст. Выберите пример или попросите ИИ-ассистента создать инфографику.", galleryExploreOfficial: "Открыть галерею", galleryUseSample: "Загрузить пример", + galleryTemplatesButton: "Галерея", gallerySearchPlaceholder: "Поиск примеров или типов…", galleryCategoryAll: "Все", + galleryConfirmReplace: "Новый пример заменит текущее содержимое холста. Продолжить?", + sourceHelp: "Синтаксис является редактируемым источником этой инфографики. Предварительный просмотр обновляется по мере редактирования.", readOnly: "Только для чтения", + }, + companion: { + learning: { rule: { move: "Обычно вы отправляете заметки с тегом «{{tag}}» в «{{notebook}}».", tag: "Вы часто добавляете «{{tag}}» в заметки в «{{notebook}}»." }, "basedOn": "Использованные воспоминания", "help": "Обучение смотрит, куда вы позже раскладываете заметки и какие теги ставите, и так улучшает подсказки. Пауза сохраняет воспоминания. Если отключить использование памяти, воспоминания не подставляются ни в чат, ни в открытия. Подходящие воспоминания отправляются вместе с выбранными заметками в модель по умолчанию.", "learningEnabled": "Учиться на моих выборах", "useMemory": "Использовать сохранённую память", "active": "Выведено из повторяющихся выборов · активно", "candidate": "Возможное предпочтение · пока не используется", "conflicted": "Мало данных или они противоречат друг другу · на паузе", "evidence": "Показать выборы, на которых это основано", "feedback": "Больше не предлагать такую раскладку", "feedbackHelp": "Запоминает предпочтение для этих блокнотов. Его можно исправить или забыть в личной памяти."}, + discovery: { + settingsTitle: "Режим Paw", + title: "Открытия из режима Paw", + description: "При включении EdgeEver тихо помогает укладывать разрозненные фрагменты вместе, добавлять новые идеи к существующим заметкам и выводить на поверхность соответствующие прошлые идеи, такие как нежная кошачья лапа.", + tagline: "Чем дольше вы им пользуетесь, тем лучше вас понимает режим Paw.", + enable: "Включить режим Paw", + settingsFailed: "Настройки не сохранены. Проверьте подключение или обновите и повторите попытку.", + modelRequired: "Настройте и включите модель ИИ по умолчанию перед включением режима Paw.", + configureModel: "Открытые интеграции ИИ", + openCompanion: "Разговор и личная память", + openCompanionDesc: "Просмотрите сохраненные предпочтения, варианты поддержки и беседы.", + panelDescription: "Не нужно обрабатывать каждый элемент. Только утвержденные операции меняют примечания; обнаружения исчезают через 7 дней.", + unread: "Новые открытия", + empty: "Ничто не стоит того, чтобы прерывать вас. Продолжайте фиксировать свои идеи.", + loadFailed: "Обнаружения недоступны. Попробуйте открыть эту панель позже.", + actionFailed: "Операция или синхронизация не завершены. Проверьте квитанцию и текущие примечания, прежде чем повторить попытку.", + appendHelp: "Добавляет исходный текст фрагмента к целевой заметке и сохраняет обе заметки. Никакого автоматического удаления или перезаписи. В настоящее время рекомендуется только для текстовых заметок.", + dismiss: "Пропустить это", + kind: { move: "Предложение · Примечания к файлу", tag: "Предложение · Добавить теги", insight: "Подключение к знаниям · Действия не требуются", merge: "Предложение · Объединить идеи", append: "Предложение · Добавить к существующему примечанию" }, + types: { insight: "Информация о компании", tag: "Теги", merge: "Объединить", append: "Добавить после", tool: "Действие" }, + checkStatus: { quiet: "Проверки проводятся по мере необходимости и могут повлечь за собой расходы на модель. Отсутствие полезного результата означает отсутствие уведомления.", ready: "Последняя проверка завершена.", running: "Проверка соответствующих заметок.", failed: "Последняя проверка не закончилась. Проверьте модель по умолчанию и подключение; эта попытка не повторяется автоматически." }, + transparency: { + title: "Статус лапы", + state: { disabled: "не запущено", quiet: "Спокойное наблюдение", ready: "Последняя проверка завершена", running: "Проверка", failed: "Проверка не завершена" }, + lastCheck: "Последняя проверка", + neverChecked: "Не проверено", + nextCheck: "Следующая проверка", + nextCheckEnabled: "Через 3 минуты бездействия: изменения заметки проверяются ежедневно; изменения памяти могут инициировать более раннюю проверку, не чаще одного раза в час", + nextCheckDisabled: "Проверки начинаются после включения режима Paw", + scope: "Каждая проверка отправляет только небольшую подборку соответствующих заметок с этой учетной записи в модель по умолчанию. Он остается тихим без полезного результата, и каждое изменение заметки требует вашего одобрения.", + latest: "Последнее открытие", + noDiscovery: "Ни одно открытие не стоило того, чтобы вас прерывать. Режим Paw будет спокойно ждать новых изменений заметок.", + }, + }, + "actions": { + "confirm": "Подтвердить", + "confirmMerge": "Подтвердить объединение", + "confirmTags": "Подтвердить дополнительные теги", + "confirmTool": "Подтвердить эту операцию", + "receipt": "Показать запись выполнения", + "affected": "Затронуто заметок: {{count}}", + "toolHelp": "Выполнятся только перечисленные аргументы; при подтверждении ИИ ничего не решает заново. Предложения действуют 24 часа. Если заметка или блокнот изменились, нужно новое предложение. Операции с несколькими записями не гарантируют полный откат; неясный результат никогда не повторяется сам.", + "tools": { + "create_memo": "Создать заметку", + "import_memos": "Импортировать заметки", + "update_memo": "Обновить заметку", + "trash_memos": "В корзину", + "restore_memos": "Восстановить заметки", + "move_memos": "Переместить заметки", + "add_tags_to_memos": "Добавить теги", + "remove_tags_from_memos": "Убрать теги", + "rename_tag": "Переименовать тег", + "delete_tag": "Удалить тег", + "merge_memos": "Объединить заметки", + "restore_memo_revision": "Восстановить версию", + "move_notebook": "Переместить блокнот", + "create_notebook": "Создать блокнот", + "rename_notebook": "Переименовать блокнот" + }, + "fields": { + "memoId": "Целевая заметка", + "memoIds": "Целевые заметки (порядок выполнения)", + "notebookId": "Блокнот", + "parentId": "Родительский блокнот", + "title": "Название", + "contentMarkdown": "Полная замена содержания", + "tags": "Теги", + "name": "Имя", + "from": "Исходный тег", + "to": "Новый тег", + "tag": "Тег", + "revisionId": "Идентификатор версии", + "expectedRevision": "Ожидаемая версия содержания", + "isPinned": "Закреплено", + "sortOrder": "Порядок сортировки", + "createdAt": "Время создания", + "updatedAt": "Время изменения", + "source": "Источник импорта", + "items": "Элементы импорта", + "dryRun": "Только просмотр" + }, + "effects": { + "merge_memos": "При объединении сохраняются тексты, вложения и теги. Исходные заметки попадают в корзину, их публичные ссылки отзываются. Вернуть исходные заметки — это не полный откат.", + "trash_memos": "Эти заметки попадут в корзину, их публичные ссылки будут отозваны. Это не безвозвратное удаление.", + "rename_tag": "Обновляет каждую заметку с этим тегом в текущей учётной записи.", + "delete_tag": "Снимает этот тег со всех затронутых заметок в текущей учётной записи и не удаляет текст заметок.", + "update_memo": "Указанные поля заменяют текущие значения. Полный текст и список тегов заменяются целиком, а не дополняются. Проверьте перед подтверждением.", + "restore_memo_revision": "Заменяет текущее содержание выбранной прошлой версией. Сначала проверьте заметку и версию." + }, + "merge": "Предлагаемое объединение идей", + "tag": "Предлагаемые дополнительные теги", + "resultTitle": "Объединённое название: {{title}}", + "existingTags": "Уже есть теги: {{tags}}", + "addTags": "Добавить только: {{tags}}", + "dismiss": "Отклонить предложение", + "openResult": "Открыть обновлённую заметку", + "conflict": "Заметки, память или само предложение изменились, либо предложение истекло. Новые изменения не применялись. Запросите новый план раскладки.", + "unsynced": "Подключитесь к сети, дождитесь синхронизации заметок и разрешите конфликты синхронизации, прежде чем подтверждать этот план.", + "syncFailed": "Изменения применены, но это устройство не смогло обновиться. Синхронизируйте вручную, чтобы увидеть результат, и не раскладывайте это ещё раз.", + "status": { "applied": "Применено", "dismissed": "Отклонено", "unavailable": "Ещё не готово или уже недействительно. Запросите новый план.", "uncertain": "Выполнение началось, но результат не подтверждён: часть изменений могла примениться. Обновите страницу и проверьте заметки. Эта операция больше не запустится." } + }, + "title": "Чат с EdgeEver", + "navTitle": "EdgeEver", + "primaryNavigation": "Основная навигация", + "preview": "Просмотр", + "backToNotes": "К заметкам", + "settings": "Настройки", + "emptyTitle": "От заметок к вашим идеям", + "privacyTitle": "Отправленное уходит вашему поставщику модели по умолчанию · данные и разрешения", + "unavailableHelp": "Это ваш отдельный чат и пространство памяти. Личная память не читается и не сохраняется в демо-режиме, без включённого входа и пока вы не вошли. Используйте не демо-копию со включённым входом и войдите в личную учётную запись. Перед чатом настройте модель по умолчанию в разделе интеграции ИИ в личных настройках.", + "intro": "Не только правка заметок: EdgeEver запоминает предпочтения, которыми вы сами делитесь.", + "privacy": "Используется настроенная модель ИИ по умолчанию. При отправке этому поставщику уходят переписка, включённые воспоминания и найденные фрагменты заметок. Память и чаты хранятся на сервере этой копии, а не только на вашем устройстве.", + "chat": "Чат", + "memories": "Что помнит Paw", + "history": "Недавние разговоры", + "newChat": "Новый разговор", + "waiting": "Ответа пока нет", + "rememberMessage": "Запомнить моё сообщение", + "stop": "Остановить", + "memoryHelp": "Paw отличает явно указанные предпочтения от выведенных привычек. Возможные и противоречивые воспоминания не используются. Ваши правки важнее. Не больше 50 воспоминаний, по 500 символов каждое.", + "correct": "Исправить", + "addMemory": "Что запомнить", + "forget": "Забыть", + "fromMessage": "Источник: сообщение, которое вы явно сохранили", + "fromManual": "Источник: добавлено или изменено вами вручную", + "backupHelp": "Данные просмотра не входят в ZIP-копии заметок и в офлайн-синхронизацию мобильного и настольного приложений. Память, чаты и записи раскладки экспортируйте отдельно. Импорт объединяет память и настройки обучения, но не чаты и не записи раскладки; привезённые выводы нужно подтвердить. Экспорт содержит открытый текст, храните его бережно.", + "export": "Экспорт памяти и чатов", + "import": "Импорт памяти", + "clearHistory": "Очистить историю чата", + "useMemory": "Использовать сохранённую память", + "allowNotes": "Разрешить инструменты заметок (изменения нужно подтверждать)", + "contextHelp": "Без разрешения инструменты заметок не запускаются. Когда они включены, ИИ может читать заметки, теги, блокноты и историю. Создание, обновление, объединение, перемещение, теги, корзина и восстановление версии показывают точные аргументы для вашего подтверждения. Если память выключена, этот разговор может продолжаться по прежним репликам, где память не использовалась. Модель вызывается только когда вы отправляете сообщение.", + "message": "О чём хотите поговорить?", + "send": "Отправить", + "clearHelp": "Безвозвратно удаляет все чаты, открытия и записи операций и останавливает генерацию. Память и настройки открытий остаются. Ранее пропущенные группы источников могут появиться снова. Если нужно, сначала сделайте экспорт.", + "forgetHelp": "Удаляет это воспоминание и данные, на которых оно выучилось, запрещает автоматически выучить то же правило снова, останавливает текущую генерацию и не подставляет старые ответы в будущий автоматический контекст. Уже написанный текст чата, экспортированные копии и данные у поставщика не удаляются: их нужно убрать отдельно.", + "configureModel": "Перед чатом укажите модель по умолчанию в настройках ИИ.", + "clientUpdateRequired": "Обновите копию EdgeEver и клиент, затем попробуйте снова.", + "credentialsRejected": "Учётные данные модели по умолчанию отклонены. Проверьте URL поставщика и ключ API в настройках ИИ.", + "providerPaymentRequired": "Поставщик модели сообщил, что не хватает средств. Проверьте баланс и попробуйте снова.", + "providerRateLimited": "Поставщик модели ограничил частоту запросов. Подождите немного и попробуйте снова.", + "providerRequestRejected": "Модель по умолчанию отклонила запрос. Убедитесь, что она поддерживает вызов инструментов, или выберите другую модель по умолчанию.", + "failureDetails": { + "timeout": "Запрос к модели не уложился во время. Попробуйте позже; если повторится, проверьте службу модели и сетевой прокси.", + "server": "Служба модели вернула ошибку сервера. Попробуйте позже; если повторится, проверьте состояние поставщика.", + "http": "Запрос отклонён. Проверьте URL поставщика, идентификатор модели и поддерживаемые возможности запроса.", + "network": "Это устройство не смогло напрямую подключиться к службе модели. Проверьте URL и сеть. Доступ из браузера также требует, чтобы служба модели разрешала CORS.", + "invalidResponse": "Поставщик вернул несовместимый ответ. Проверьте выбранный протокол API и поддержку потоковой передачи.", + "emptyResponse": "Модель не вернула текст и не завершила вызовы инструментов.", + "responseLimit": "Ответ модели превысил предел разговора.", + }, + "conflict": "Воспоминание изменено в другом месте, либо достигнут предел в 50 воспоминаний. Обновите и попробуйте снова.", + "historyFull": "Чаты и открытия вместе достигли предела в 500 записей. Экспортируйте и очистите историю, прежде чем продолжать.", + "unavailable": "Просмотр доступен вошедшему личному аккаунту на копии, которая не является демо.", + "failed": "Операция не завершилась. Проверьте соединение и настройки модели по умолчанию, затем обновите страницу, чтобы увидеть сохранённый результат. Вызовы модели сами не повторяются.", + "recovered": "Генерация остановилась или соединение оборвалось. Сохранённое содержание восстанавливается; обновите страницу, чтобы увидеть итог. Генерация сама не повторится.", + "status": { + "running": "Генерация (обновите, чтобы восстановить)", + "completed": "Завершено", + "failed": "Не вышло · часть текста могла остаться", + "cancelled": "Остановлено", + "interrupted": "Прервано" + }, + timeline: { + title: "Этот запуск", + running: "В процессе", + error: "Не завершено", + open: "Открыть заметку", + restore: "Восстановить из корзины", + restoreRevision: "Восстановить предыдущую версию", + restored: "Восстановлено", + restoreFailed: "Не удалось восстановить. Проверьте корзину или историю версий в заметке.", + }, + mentions: { + hint: "Введите @, чтобы закрепить заметку, блокнот или тег", + notes: "Заметки", + notebooks: "Блокноты", + tags: "Теги", + empty: "Нет подходящих заметок, блокнотов или тегов", + remove: "Убрать {{title}}", + }, + todos: { + title: "Задачи", + pending: "К выполнению", + inProgress: "В процессе", + completed: "Готово", + }, + questions: { + title: "Нужен выбор", + submit: "Продолжить", + other: "Другое", + }, + continue: "Продолжить незавершённую работу", + process: "Думаю", + trace: "Запуск · {{count}} шагов", + }, + common: { + appName: "EdgeEver", + close: "Закрыть", + cancel: "Отмена", + save: "Сохранить", + delete: "Удалить", + loading: "Загрузка", + back: "Назад", + language: "Язык", + untitledMemo: "Без названия", + processing: "Обработка", + saving: "Сохранение", + copied: "Скопировано", + refresh: "Обновить", + githubRepository: "Открыть источник", + }, + dialogs: { + memoDelete: { + permanentBulkTitle: "Удалить {{count}} заметок безвозвратно", + permanentSingleTitle: "Удалить заметку безвозвратно", + softBulkTitle: "Удалить {{count}} заметок", + softSingleTitle: "Удалить заметку", + permanentDescription: "Это действие нельзя отменить.", + softDescription: "Удалённые заметки можно восстановить из корзины.", + permanentConfirm: "Удалить навсегда", + softConfirm: "Удалить", + }, + notebookName: { + createTitle: "Новый блокнот", + renameTitle: "Переименовать блокнот", + createDescription: "Создайте новый блокнот, чтобы объединить связанные заметки.", + renameDescription: "Изменения сразу синхронизируются с деревом блокнотов.", + nameLabel: "По имени", + namePlaceholder: "Название блокнота", + createSubmit: "Создать", + }, + }, + login: { + title: "Вход в EdgeEver", + subtitle: "Ваше личное self-hosted пространство заметок", + username: "Имя пользователя", + password: "Пароль", + submitting: "Вход…", + desktopStarting: "Запуск EdgeEver", + desktopStartingDescription: "Подключение к локальному сервису. Это может занять немного времени.", + desktopPreparingData: "Подготовка локальных данных", + desktopPreparingDataDescription: "Переключение на локальное пространство данных текущего аккаунта.", + submit: "Открыть рабочее пространство", + authNotConfigured: "Вход не настроен для этого инстанса. Обратитесь к администратору.", + databaseNotReady: "Этот инстанс ещё не готов. Обратитесь к администратору.", + instanceUnavailable: "Этот инстанс временно недоступен. Проверьте логи Worker и выполните bun run deploy:doctor для проверки развёртывания.", + instanceUnreachable: "Не удаётся подключиться к инстансу. Проверьте адрес и сеть.", + instanceApiNotFound: "Этот адрес не является инстансом EdgeEver. Проверьте адрес инстанса.", + invalidCredentials: "Неверное имя пользователя или пароль. Проверьте оба поля и попробуйте снова.", + sessionExpired: "Сессия истекла. Войдите снова.", + loginRateLimited: "Слишком много попыток. Попробуйте позже.", + cloudflareChallenge: "Браузерная проверка Cloudflare перехватила API входа и не может выполниться на этой странице. Попросите администратора инстанса настроить правило WAF Challenge для входа.", + securityPolicyBlocked: "Политика безопасности заблокировала запрос до EdgeEver. Попросите администратора инстанса проверить Cloudflare Security Events, Access, WAF или правила обратного прокси.", + requestForbidden: "Инстанс отклонил запрос на вход (HTTP 403). Попросите администратора инстанса проверить политику доступа.", + instanceServerError: "Инстанс временно недоступен. Попробуйте позже.", + requestRejected: "Инстанс отклонил запрос на вход (HTTP {{status}}). Обратитесь к администратору инстанса.", + invalidInstanceResponse: "Это недействительный инстанс EdgeEver. Проверьте адрес инстанса.", + unexpectedError: "Не удалось войти. Попробуйте снова.", + diagnosticCode: "Код ошибки: {{code}}", + cloudflareRayId: "CF-Ray: {{id}}", + desktopScopeUnavailable: "Не удалось переключить область локальных данных на десктопе. Рабочее пространство приостановлено для защиты данных аккаунта.", + desktopScopeRetry: "Повторить", + passwordHashInvalid: "Этот аккаунт настроен неверно. Попросите администратора сбросить пароль.", + desktopInstanceTitle: "Подключить инстанс EdgeEver", + desktopInstanceUrl: "URL инстанса", + desktopInstanceContinue: "Продолжить", + desktopChangeInstance: "Изменить URL инстанса", + desktopInstanceUrlInvalid: "Введите корректный URL инстанса.", + instanceUrlPlaceholder: "https://demo.edgeever.org", + }, + nav: { + mobileMain: "Основная навигация", + home: "Главная", + templates: "Шаблоны", + prompts: "Промпты", + mine: "Я", + createMemo: "Новая заметка", + createDisabled: "В этом разделе нельзя создать заметку", + creating: "Создание", + }, + structuredTable: { + name: "База данных", + title: "Название базы данных", + addField: "Добавить поле", + addRecord: "Добавить запись", + deleteField: "Удалить поле", + deleteRecord: "Удалить эту запись", + fieldName: "Название поля", + fieldType: "Тип поля", + selectOptions: "Варианты", + selectOptionsHint: "Через запятую", + filter: "Фильтр", + addFilter: "Добавить фильтр", + clearFilters: "Сбросить фильтры", + filterValue: "Значение фильтра", + sort: "Сортировка", + noSort: "Без сортировки", + ascending: "По возрастанию", + descending: "По убыванию", + empty: "Пока нет записей", + noMatches: "Нет записей по фильтрам", + exportCsv: "Экспорт CSV", + recordCount: "{{count}} записей", + recordCountFiltered: "{{count}} / {{total}} записей", + listCounts: "{{records}} записей · {{fields}} полей", + newField: "Поле {{index}}", + fieldLimit: "До 40 полей", + recordLimit: "До 2 000 записей", + unreadable: "Не удалось прочитать базу данных", + saveError: "Не удалось сохранить базу данных", + editSessionError: "Не удалось начать безопасный сеанс редактирования. Попробуйте позже.", + saving: "Сохранение", + saved: "Сохранено", + unsaved: "Есть несохранённые изменения", + back: "К списку заметок", + editField: "Изменить поле {{name}}", + openForm: "Сбор через форму", + openFormTooltip: "Поделитесь ссылкой на форму, чтобы собирать ответы как записи таблицы.", + formLive: "Сбор ответов", + formLiveTooltip: "Принимает ответы. Откройте, чтобы скопировать ссылку или остановить сбор.", + refresh: "Обновить", + refreshTooltip: "Обновить таблицу", + refreshError: "Не удалось обновить таблицу", + form: { + title: "Сбор через форму", + description: "Поделитесь ссылкой на форму. Ответы добавляются как новые записи таблицы; респонденты не видят существующие строки.", + enabled: "Принимать ответы", + link: "Ссылка на форму", + copy: "Копировать ссылку", + copied: "Скопировано", + password: "Пароль доступа", + passwordHint: "При включении создаётся пароль. Он нужен, чтобы открыть форму.", + regenerate: "Сгенерировать новый пароль", + formTitle: "Заголовок формы", + formDescription: "Описание", + submitLabel: "Текст на кнопке", + submitDefault: "Отправить", + fields: "Поля формы", + required: "Обязательное", + saving: "Сохранение", + saved: "Форма сохранена", + needField: "Выберите хотя бы одно поле", + closed: "Форма закрыта", + full: "Форма больше не принимает ответы", + submitted: "Отправлено", + submitAnother: "Отправить ещё", + passwordRequired: "Введите пароль доступа", + unlock: "Открыть форму", + upload: "Загрузить вложение", + uploadLimit: "Каждое вложение — не больше 20 МБ", + }, + addAttachment: "Добавить вложение", + openAttachment: "Открыть {{name}}", + copyImage: "Копировать изображение", + imageCopied: "Скопировано. Можно вставить.", + copyImageFailed: "Не удалось скопировать изображение", + downloadAttachment: "Скачать {{name}}", + removeAttachment: "Удалить {{name}}", + attachmentLimit: "До 10 вложений в ячейке", + uploading: "Загрузка", + types: { + text: "Текст", + number: "Число", + checkbox: "Флажок", + date: "Дата", + select: "Список", + url: "Ссылка", + attachment: "Вложение", + }, + operators: { + contains: "содержит", + eq: "равно", + empty: "пусто", + notEmpty: "не пусто", + }, + defaultFields: { + name: "По имени", + status: "Статус", + date: "Дата", + sample: "Пример записи", + notStarted: "Не начато", + inProgress: "В процессе", + done: "Готово", + }, + }, + diagram: { + seed: { + mindMapRoot: "Главная тема", + mindMapCapture: "Записать идеи", + mindMapOrganize: "Упорядочить", + mindMapShare: "Поделиться", + mindMapQuickNotes: "Быстрые заметки", + mindMapSync: "Синхронизация устройств", + mindMapNotebooks: "Блокноты", + mindMapPublicLinks: "Публичные ссылки", + flowchartStart: "Начало", + flowchartProcess: "Шаг процесса", + flowchartEnd: "Конец", + architectureSystem: "Приложение", + architectureClient: "Веб-клиент", + architectureApi: "Сервис API", + architectureDatabase: "База данных", + architectureStorage: "Хранилище файлов", + architectureQuery: "Чтение / запись", + architectureFiles: "Файлы", + }, + normalNote: "Обычная заметка", + mindMap: "Ментальная карта", + flowchart: "Блок-схема", + architecture: "Архитектурная схема", + createType: "Выбрать тип заметки", + moreTypes: "Ещё", + back: "К списку заметок", + title: "Название схемы", + canvas: "Холст: {{type}}", + canvasView: "Вид холста", + outlineView: "Вид структуры", + outlineUnavailable: "Структура доступна, только если есть ровно одна корневая тема", + outlineHint: "Корень — заголовок # с текстом, ветки — вложенные пункты списка -. Два пробела на уровень. Пустые пункты скрыты. Enter продолжает список, Tab и Shift+Tab меняют уровень.", + outlineErrorEmpty: "Оставьте хотя бы одну корневую тему.", + outlineErrorIndent: "Неверный отступ в строке {{line}}. Используйте табы или два пробела на уровень.", + outlineErrorRoot: "Неверная иерархия в строке {{line}}. Нужна ровно одна корневая тема.", + outlineErrorDepth: "Строка {{line}} пропускает уровень. Отступ только на один уровень глубже предыдущей темы.", + outlineErrorLabel: "Тема в строке {{line}} должна содержать от 1 до 500 символов.", + outlineRemovalWarning: "Правка удалит {{count}} существующих тем. Карта не сохранится, пока вы не подтвердите.", + outlineApplyRemoval: "Удалить темы и применить", + outlineRestore: "Вернуть содержимое карты", + addTopic: "Добавить дочернюю тему", + addSiblingTopic: "Добавить соседнюю тему", + mindMapShortcuts: "Tab — дочерний элемент; Enter — соседний", + addStep: "Добавить шаг", + flowchartShortcuts: "Tab — соединить со следующим; Enter — редактировать; Cmd/Ctrl+D — дублировать; стрелки — сдвинуть", + architectureShortcuts: "Перетаскивайте маркеры, чтобы соединить компоненты; Enter — редактировать; Cmd/Ctrl+D — дублировать", + addDecision: "Добавить решение", + addTerminator: "Добавить начало или конец", + addClient: "Добавить клиент", + addFrontend: "Добавить frontend", + addService: "Добавить сервис", + addDatabase: "Добавить базу данных", + addStorage: "Добавить хранилище", + addQueue: "Добавить очередь", + addSecurity: "Добавить компонент безопасности", + addExternal: "Добавить внешнюю систему", + addBoundary: "Добавить границу системы", + components: "Компоненты", + componentLibrary: "Библиотека фигур", + placeShapeHelp: "Перетащите на холст или выберите и кликните для размещения", + placeShapeHint: "Кликните по холсту, чтобы разместить «{{shape}}» · Esc — отмена", + componentSearch: "Поиск компонентов архитектуры", + componentCategoryExperience: "Приложения и клиенты", + componentCategoryServices: "Вычисления и сервисы", + componentCategoryAi: "ИИ и машинное обучение", + componentCategoryDatabases: "Базы данных", + componentCategoryStorage: "Хранилище и CDN", + componentCategoryMiddleware: "Интернет-middleware", + componentCategoryNetwork: "Сеть", + componentCategorySecurity: "Безопасность и границы", + componentCategoryObservability: "Управление и мониторинг", + componentCategoryExternal: "Внешние сервисы", + noMatchingComponents: "Нет подходящих компонентов архитектуры", + architectureResources: { + client: "Клиент", + webApp: "Веб-приложение", + mobileApp: "Мобильное приложение", + website: "Сайт", + apiClient: "API-клиент", + service: "Сервис приложения", + cpu: "Ядро CPU", + gpu: "GPU", + memory: "Память", + virtualMachine: "Виртуальная машина", + container: "Контейнер", + kubernetes: "Kubernetes", + serverless: "Serverless-функция", + largeLanguageModel: "Большая языковая модель", + multimodalModel: "Мультимодальная модель", + embeddingModel: "Модель эмбеддингов", + reranker: "Модель реранкинга", + modelInference: "Сервис инференса моделей", + vectorDatabase: "Векторная база данных", + ragPipeline: "RAG-конвейер", + aiAgent: "ИИ-агент", + modelGateway: "Шлюз моделей", + mcpServer: "MCP-сервер", + relationalDatabase: "Реляционная БД", + noSqlDatabase: "NoSQL-база данных", + cache: "Кэш", + dataWarehouse: "Хранилище данных", + searchEngine: "Поисковый движок", + objectStorage: "Объектное хранилище", + fileStorage: "Файловое хранилище", + blockStorage: "Блочное хранилище", + backup: "Резервное копирование", + cdn: "CDN", + messageQueue: "Очередь сообщений", + eventBus: "Шина событий", + streamProcessing: "Потоковая обработка", + webhook: "Webhook", + serviceMesh: "Сервисная сеть", + apiGateway: "API-шлюз", + loadBalancer: "Балансировщик нагрузки", + dns: "DNS", + vpc: "Виртуальное частное облако", + subnet: "Подсеть", + vpn: "VPN", + identity: "Идентификация и доступ", + firewall: "Межсетевой экран", + waf: "WAF", + secretManager: "Менеджер секретов", + certificate: "Сертификат", + systemBoundary: "Граница системы", + monitoring: "Мониторинг", + logging: "Логирование", + metrics: "Метрики", + tracing: "Распределённая трассировка", + alerting: "Оповещения", + saas: "SaaS-сервис", + externalApi: "Внешний API", + thirdPartyService: "Сторонний сервис", + }, + newTopic: "Новая тема", + newStep: "Новый шаг", + newDecision: "Решение", + newTerminator: "Начало / конец", + newClient: "Клиент", + newFrontend: "Фронтенд", + newService: "Сервис", + newDatabase: "База данных", + newStorage: "Хранилище", + newQueue: "Очередь сообщений", + newSecurity: "Безопасность", + newExternal: "Внешняя система", + newBoundary: "Граница системы", + quickCreateTitle: "Выберите тип следующего узла", + quickCreateShortcuts: "Нажмите 1 / 2 / 3 для выбора; Esc — отмена", + nodeText: "Текст узла", + nodeIcon: "Значок узла", + changeNodeIcon: "Сменить значок узла", + edgeText: "Подпись связи", + editNode: "Изменить текст узла", + deleteSelection: "Удалить выделенное", + navHintPan: "Перетащите, чтобы сдвинуть холст", + navHintFocusRelations: "Наведите на узел или связь, чтобы увидеть отношения", + navHintBoxSelect: "для выделения рамкой", + navHintAddChild: "Дочерний узел", + navHintAddSibling: "Соседний узел", + undo: "Отменить", + redo: "Повторить", + zoomIn: "Увеличить", + zoomOut: "Уменьшить", + zoomPercent: "Масштаб в процентах", + listCounts: "{{nodes}} узлов · {{edges}} связей", + resetZoom: "Сбросить на 100% и центрировать", + readFlow: "Читать с начала", + readFlowHint: "Чтение при 100%; прокрутка или перетаскивание — сдвиг, Ctrl / ⌘ + прокрутка — масштаб", + fit: "Вписать диаграмму", + autoLayout: "Автоматическое размещение", + autoLayoutTooltip: "Перестроить все узлы и связи", + allEdgeLabels: "Все подписи", + showAllEdgeLabels: "Показывать все подписи связей", + hideDenseEdgeLabels: "Показывать подписи только у выбранной связи", + toolbar: "Панель диаграммы", + theme: "Цветовая схема", + themeGroupVivid: "Яркая", + themeGroupClassic: "Спокойная", + themePlain: "Простая", + themeBrand: "Лес", + themeCosmos: "Космос", + themeDune: "Дюна", + themeSlate: "Сланец", + themePrism: "Призма", + themeSunrise: "Рассвет", + themeMarine: "Морской", + themeBlossom: "Цветение", + themeMint: "Мята", + themeMacaron: "Макарон", + themeInk: "Чернила", + themePaper: "Бумага", + themeSun: "Блеск", + themeWa: "Прилив", + themeIsland: "Глина", + themeRose: "Румянец", + themeTea: "Росток", + themeNaive: "Шипучий", + structure: "Фигура", + structureGroupMap: "Ментальная карта", + structureGroupLogic: "Логическая схема", + structureGroupBrace: "Карта-скобка", + structureGroupOrg: "Оргструктура", + structureGroupTree: "Дерево", + structureGroupTimeline: "Срок", + structureGroupFishbone: "Рыбий скелет", + structureLogic: "Логика", + structureMap: "Карта", + structureLine: "Линия", + structureCapsule: "Капсула", + structureBox: "Прямоугольник", + structureCircle: "Круг", + structureEllipse: "Эллипс", + structureHexagon: "Шестиугольник", + structureTree: "Дерево", + structureBrace: "Скобка", + structureOrg: "Орг", + structureTimeline: "Срок", + structureFishbone: "Рыбий скелет", + export: "Экспорт", + exportPng: "Экспорт PNG", + exportSvg: "Экспорт файла SVG", + exportError: "Не удалось экспортировать диаграмму", + save: "Сохранить", + retrySave: "Повторить сохранение", + saving: "Сохранение", + saved: "Сохранено", + unsaved: "Есть несохранённые изменения", + saveError: "Не удалось сохранить диаграмму", + editSessionError: "Не удалось открыть безопасный сеанс редактирования. Попробуйте чуть позже.", + discardTitle: "Отбросить несохранённые изменения?", + discardDescription: "Возврат к списку заметок приведёт к потере несохранённых изменений диаграммы.", + discard: "Отбросить изменения", + }, + workspace: { + inboxDeleteTitle: "Входящие нельзя удалить", + inboxDeleteDescription: "«Входящие» — блокнот по умолчанию, чтобы новым заметкам всегда было куда сохраняться.", + selection: { + pin: "Закрепить", + unpin: "Открепить", + chooseMemo: "Выберите заметку", + trashCannotPin: "Заметки в корзине нельзя закрепить", + updatingPin: "Обновление закрепления", + trashCannotMove: "Заметки в корзине нельзя перемещать", + noMovableNotebook: "Нет доступных блокнотов", + moving: "Перемещение", + move: "Переместить", + alreadyInNotebook: "Выбранные заметки уже в этом блокноте", + needTwoMemos: "Выберите не менее 2 заметок", + trashCannotMerge: "Заметки в корзине нельзя объединять", + merging: "Объединение", + merge: "Объединить заметки", + export: "Экспорт Markdown", + exportHint: "Упаковать последнюю сохранённую версию выбранных заметок в ZIP с Markdown и вложениями", + exporting: "Экспорт", + exportProgress: "Экспорт {{completed}} / {{total}}", + trashCannotExport: "Заметки в корзине нельзя экспортировать", + exportTooMany: "За раз можно экспортировать не более {{max}} заметок", + exportLocalOnly: "Несинхронизированные заметки нельзя экспортировать. Сначала синхронизируйте их и попробуйте снова.", + exportEmpty: "Нет заметок для экспорта", + exportError: "Не удалось экспортировать. Проверьте подключение к сети и попробуйте снова.", + exportCompleteWithSkipped: "Начата загрузка ZIP с Markdown. Пропущено несинхронизированных заметок: {{count}}.", + deleting: "Удаление", + permanentDelete: "Удалить навсегда", + delete: "Удалить", + }, + loading: { + notebooks: "Загрузка блокнотов", + settings: "Загрузка профиля", + assets: "Загрузка ресурсов", + tags: "Загрузка тегов", + migration: "Загрузка руководства по миграции", + editor: "Загрузка редактора", + }, + pullToRefresh: { + refreshingNotes: "Получение последних заметок", + refreshingPage: "Обновление страницы", + releaseNotes: "Отпустите для обновления", + releasePage: "Отпустите для обновления страницы", + pullNotes: "Потяните для обновления", + pullPage: "Потяните для обновления страницы", + }, + }, + notebookPane: { + notebooks: "Блокноты", + workspaceFallback: "Рабочее пространство заметок", + backToList: "К списку заметок", + newNotebook: "Новый блокнот", + newMemo: "Новая заметка", + entries: "Заметки", + allMemos: "Все заметки", + sortTitle: "Сортировка блокнотов: {{label}}", + loading: "Загрузка", + secondaryEntries: "Дополнительно", + profile: "Профиль", + collapseSidebar: "Свернуть панель блокнотов", + expandSidebar: "Развернуть панель блокнотов", + trash: "Корзина", + emptyTrash: "Очистить корзину", + empty: "Пусто", + syncNow: "Синхронизировать", + sync: { + offlineWithPending: "Офлайн, ожидает: {{count}}", + offline: "Офлайн", + syncing: "Синхронизация", + conflicts: "Конфликтов синхронизации: {{count}}", + discardConflicts: "Взять облачные версии", + discardingConflicts: "Применение облачных версий", + retry: "{{count}} элементов синхронизации требуют внимания", + pending: "{{count}} в ожидании", + synced: "Синхронизировано", + }, + syncDetails: { + title: "Сведения о синхронизации", + description: "Неудавшиеся элементы надёжно хранятся на этом устройстве и больше не мешают изменениям с других устройств попадать в приложение для компьютера.", + empty: "Нет элементов синхронизации, требующих внимания.", + globalFailure: "Сбой синхронизации с компьютером", + attempts: "Попыток: {{count}}", + unknownError: "Неизвестная ошибка синхронизации", + errorCode: "Код ошибки: {{code}}", + lastAttempt: "Последняя попытка: {{time}}", + nextRetry: "Следующая попытка: {{time}}", + missingMemoHint: "Облачная заметка больше не существует. Восстановите локальные изменения как новую заметку или явно отбросьте эту локальную копию.", + retry: "Повторить сейчас", + recover: "Восстановить как новую заметку", + recoverTitle: "Восстановить локальные изменения", + recoverDescription: "Выберите блокнот. EdgeEver сохранит содержимое неудавшегося элемента в новой заметке и продолжит синхронизацию.", + recoveryNotebook: "Выберите блокнот", + discard: "Отбросить локальные изменения", + discardTitle: "Отбросить эти локальные изменения?", + discardDescription: "Содержимое, которое не было отправлено из этого элемента синхронизации, будет удалено безвозвратно. Другие заметки и облачные данные не затронуты.", + reportIssue: "Сообщить о проблеме", + reportTitlePrefix: "[Синхронизация] Синхронизация с компьютером требует внимания", + reportContentHeading: "Что произошло", + reportContentPrompt: "Опишите, что произошло, чего вы ожидали, и любые шаги, которые помогут воспроизвести проблему синхронизации.", + reportDiagnosticsHeading: "Автоматически собранная диагностика синхронизации", + reportDiagnosticsNotice: "EdgeEver сформировал и обезличил эту диагностику локально. Просмотрите её перед отправкой.", + operations: { + memo_create: "Создать заметку", + memo_update: "Обновить заметку", + memo_delete: "Удалить заметку", + memo_restore: "Восстановить заметку", + memo_move: "Переместить заметку", + memo_deleteBatch: "Удалить заметки", + memo_emptyTrash: "Очистить корзину", + memo_pinBatch: "Закрепить заметки", + memo_merge: "Объединить заметки", + notebook_create: "Создать блокнот", + notebook_update: "Обновить блокнот", + notebook_delete: "Удалить блокнот", + notebook_restore: "Восстановить блокнот", + template_create: "Создать шаблон", + template_update: "Обновить шаблон", + template_delete: "Удалить шаблон", + tag_rename: "Переименовать тег", + tag_delete: "Удалить тег", + }, + }, + }, + notebookTree: { + dragNotebook: "Переместить блокнот «{{name}}»", + expandCollapse: "Развернуть/свернуть", + collapse: "Свернуть {{name}}", + expand: "Развернуть {{name}}", + current: "Текущий: {{name}}", + switchTo: "Перейти к {{name}}", + currentWithCount: "Текущий: {{name}}, заметок: {{count}}", + switchToWithCount: "Перейти к {{name}}, заметок: {{count}}", + currentWithSplitCount: "Сейчас: {{name}}, здесь {{direct}} заметок, всего {{total}} с вложенными блокнотами", + switchToWithSplitCount: "Перейти к {{name}}, здесь {{direct}} заметок, всего {{total}} с вложенными блокнотами", + actions: "Действия с блокнотом", + actionsAria: "Открыть действия для {{name}}", + newChild: "Новый вложенный блокнот", + newChildAria: "Создать вложенный блокнот в {{name}}", + rename: "Переименовать", + renameNotebook: "Переименовать блокнот", + renameAria: "Переименовать {{name}}", + deleteNotebook: "Удалить блокнот", + deleteAria: "Удалить {{name}}", + inboxDeleteHint: "По умолчанию хранит неразобранные заметки. Можно переименовать, но нельзя удалить.", + }, + settings: { + title: "Я", + subtitle: "Настройки, сведения о системе, токены MCP и сеанс входа", + groups: { + interface: "Интерфейс", + reading: "Чтение", + editing: "Редактирование", + }, + languageTitle: "Язык интерфейса", + systemLanguage: "Как в системе", + picgo: { + webPreview: "Доступно только в приложении для компьютера.", + title: "Хостинг изображений", + builtin: "Встроенный хостинг изображений (по умолчанию)", + external: "Внешний хостинг (PicGo / PicList)", + endpoint: "Адрес сервера PicGo", + description: "PicGo на этом компьютере или удалённый PicList, например на NAS. Путь оставьте /upload.", + serverKey: "Ключ сервера", + serverKeyDescription: "Заполните, если PicList требует ключ. Если ключа нет, оставьте пустым. Ключ уходит с каждым запросом, поэтому для удалённого сервера используйте HTTPS.", + externalImages: "Изображения хранятся на внешнем хосте и не входят в резервные копии EdgeEver.", + test: "Проверить соединение", + testing: "Проверка…", + connected: "Соединение с PicGo есть. Теперь настройку можно сохранить.", + connectionFailed: "Не удалось подключиться. Проверьте адрес и ключ сервера и убедитесь, что PicGo или PicList запущен.", + save: "Сохранить", + saved: "Сохранено", + saveFailed: "Не удалось сохранить настройку.", + failedTitle: "Не удалось загрузить через PicGo", + failedMessage: "Проверьте адрес, ключ сервера и что PicGo или PicList запущен с настроенным хостингом. Повтор может загрузить изображение ещё раз.", + retry: "Повторить", + fallback: "Сохранить в EdgeEver", + cancel: "Отмена", + }, + imageCompressionTitle: "Сжимать изображения в заметках", + imageCompressionAria: "Сжимать изображения в заметках", + showDescendantNotesTitle: "Показывать заметки из вложенных блокнотов", + showDescendantNotesAria: "Показывать в родительском блокноте заметки из вложенных блокнотов", + loginItem: { + title: "Запускать при включении", + aria: "Запускать EdgeEver при включении компьютера", + requiresApproval: "Разрешите EdgeEver в настройках системы, чтобы включение завершилось.", + saveFailed: "Не удалось сохранить эту настройку.", + }, + spellcheckTitle: "Подчёркивать орфографические ошибки", + spellcheckAria: "Подчёркивать возможные орфографические ошибки в редакторе заметок", + aiSelectionMenuTitle: "Показывать ИИ при выделении текста", + aiSelectionMenuAria: "Показывать действие ИИ-помощника при выделении текста", + aiSpaceShortcutTitle: "Открывать ИИ пробелом в пустом блоке", + aiSpaceShortcutAria: "Открывать ИИ пробелом в пустом блоке", + linkOpenModifierTitle: "Открывать ссылки по Ctrl/⌘ + клик при редактировании", + linkOpenModifierAria: "Открывать веб-ссылки при редактировании только по Ctrl или ⌘ + клик", + mermaidThemeTitle: "Тема диаграмм", + markdownThemeTitle: "Тема исходной разметки Markdown", + markdownThemeDescription: "Следовать оформлению приложения или выбрать фиксированную тему подсветки синтаксиса для редактирования исходной разметки Markdown.", + editorThemeTitle: "Тема редактора", + editorThemeDescription: "Выберите стиль письма для редактора заметок. Сохраняется в этом браузере. Копирование в WeChat использует тот же стиль.", + editorContentAlignmentTitle: "Макет содержимого заметки", + editorContentAlignments: { + center: "Оптимизировать читаемость (по центру)", + start: "По левому краю", + }, + editorBodyFontSizeTitle: "Размер текста", + editorBodyLineHeightTitle: "Межстрочный интервал", + accountSyncDescription: "Синхронизируется с облаком.", + editorBodyLineHeights: { + compact: "Компактный 1.5", + standard: "Стандартный 1.65", + relaxed: "Свободный 2.0", + }, + editorBodyPaletteTitle: "Цвета текста", + editorBodyPalettes: { + native: "Системная", + clay: "Красный", + dawn: "Золотой", + teal: "Бирюзовый", + azure: "Синий", + violet: "Фиолетовый", + pink: "Розовый", + slate: "Серый", + }, + editorBodyCssTitle: "Свой CSS", + editorBodyCssDescription: "Для светлой и тёмной темы — свои правила. Размер и высота строки задаются элементами управления выше.", + editorBodyCssPreviewLabel: "Просмотр", + editorBodyCssPreviewLight: "Светлая", + editorBodyCssPreviewDark: "Тёмная", + editorBodyCssPreviewHeading: "Заголовок", + editorBodyCssPreviewBefore: "Абзац с ", + editorBodyCssPreviewLink: "ссылкой", + editorBodyCssPreviewBetween: " и ", + editorBodyCssPreviewCode: "встроенным кодом", + editorBodyCssPreviewAfter: ".", + editorBodyCssPreviewQuote: "Цитата.", + editorBodyCssDropped: "Некоторые объявления не применятся и будут удалены при сохранении.", + editorBodyCssEdit: "Изменить стили", + editorBodyCssReset: "Сбросить", + editorBodyCssPlaceholder: "p { text-indent: 2em; } h1 { letter-spacing: 0.04em; }", + editorBodyFontTitle: "Шрифт текста заметки", + editorBodyFonts: { + system: "Сведения о системе", + wenkai: "LXGW WenKai", + wenkaiScreen: "LXGW WenKai Screen", + zhuque: "Zhuque Fangsong", + sourceHanSerif: "Source Han Serif", + neoZhiSong: "LXGW Neo ZhiSong", + sourceHanSans: "Source Han Sans", + sourceSerif: "Латинский с засечками", + custom: "Своя", + }, + editorBodyFontCustomLabel: "Название шрифта", + editorBodyFontCustomPlaceholder: "LXGW WenKai", + editorBodyFontSuggestions: "Популярные шрифты", + uiFontTitle: "Шрифт интерфейса", + editorThemes: { + default: "Встроенная тема", + letter: "Тёплая страница", + guide: "Страница урока", + blueprint: "Черновик Navy", + journal: "Ведущий выпуск", + stance: "Глиняный обзор", + stub: "Янтарные заметки", + brief: "Бирюзовый абзац", + outline: "Фиолетовый указатель", + zen: "Туманная страница", + grove: "Лесные заметки", + "minimal-emerald": "Minimal Jade", + "outline-emerald": "Lined Jade", + "wechat-green": "WeChat Green", + "modern-mint": "Fresh Mint", + custom: "Своя", + }, + customEditorTheme: { + title: "Своя тема редактора", + description: "Настройте цвета заметок. Они привязаны к этому аккаунту и используются в редакторе и публичных ссылках.", + settingsTitle: "Своя тема", + defaultName: "Своя тема", + newName: "Новая тема {{n}}", + create: "Создать новую тему…", + edit: "Изменить цвета", + import: "Импорт", + export: "Экспорт", + reset: "Восстановить по умолчанию", + name: "Название темы", + background: "Фон", + text: "Основной текст", + muted: "Приглушённый текст", + heading: "Заголовки", + accent: "Акцент", + soft: "Мягкий фон", + border: "Граница", + codeBackground: "Фон кода", + previewQuote: "Блок кода внутри цитаты должен иметь отдельный фон.", + previewTitle: "Предпросмотр темы", + previewBody: "Это пример основного текста, чтобы оценить сочетание цветов.", + previewMuted: "Приглушённый вспомогательный текст должен оставаться читаемым на мягком фоне.", + previewAccent: "Акцентный текст", + contrastWarning: "У {{fields}} низкий контраст в режиме «{{mode}}». Тему всё равно можно сохранить.", + modes: { + light: "светлая тема", + dark: "тёмная тема", + }, + importErrors: { + invalidFile: "Импорт не удался: это неверный файл темы редактора EdgeEver.", + unsupportedVersion: "Импорт не удался: версия файла темы редактора не поддерживается.", + fileTooLarge: "Импорт не удался: файл темы редактора превышает 64 КиБ.", + }, + }, + mermaidThemes: { + auto: "Как в приложении", + "zinc-light": "Zinc Light", + "zinc-dark": "Zinc Dark", + "tokyo-night": "Tokyo Night", + "tokyo-night-storm": "Tokyo Night Storm", + "tokyo-night-light": "Tokyo Night Light", + "catppuccin-mocha": "Catppuccin Mocha", + "catppuccin-latte": "Catppuccin Latte", + nord: "Nord", + "nord-light": "Nord Light", + dracula: "Dracula", + "github-light": "GitHub Light", + "github-dark": "GitHub Dark", + "solarized-light": "Solarized Light", + "solarized-dark": "Solarized Dark", + "one-dark": "One Dark", + }, + markdownThemes: { + auto: "Как в приложении", + "github-light": "GitHub Light", + "github-dark": "GitHub Dark", + "one-dark": "One Dark", + "tokyo-night": "Tokyo Night", + "tokyo-night-storm": "Tokyo Night Storm", + dracula: "Dracula", + nord: "Nord", + monokai: "Monokai", + "solarized-light": "Solarized Light", + "solarized-dark": "Solarized Dark", + "vscode-dark": "VS Code Dark", + "xcode-light": "Xcode Light", + sublime: "Sublime", + "duotone-light": "Duotone Light", + "duotone-dark": "Duotone Dark", + "gruvbox-dark": "Gruvbox Dark", + }, + themeTitle: "Тема интерфейса", + themeSystem: "Как в системе", + themeLight: "Светлая", + themeDark: "Тёмная", + themeToggleToDark: "Переключить на тёмную тему", + themeToggleToLight: "Переключить на светлую тему", + tabs: { + general: "Основные", + shortcuts: "Горячие клавиши", + ai: "ИИ и интеграции", + speech: "Расшифровка аудио и видео", + mcp: "API / MCP", + data: "Импорт и экспорт", + advanced: "Дополнительно", + account: "Вход", + }, + }, + plugins: { + title: "Плагины и темы", + developerDocs: "Разработка плагинов", + manifestUrl: "URL манифеста расширения", + manifestPlaceholder: "https://example.com/edgeever-plugin/manifest.json", + installSource: "Репозиторий GitHub или URL манифеста", + sourcePlaceholder: "https://github.com/owner/edgeever-plugin", + install: "Установить", + installing: "Установка…", + empty: "В этом рабочем пространстве ещё нет установленных плагинов или тем.", + syncDescription: "Список установок привязан к этому рабочему пространству в вебе и на компьютере. Настройки и секреты остаются на этом устройстве.", + permissions: { + publicNetwork: "Читать содержимое с любого публичного сайта", + }, + trustWarning: { + title: "Включить сторонние плагины?", + description: "Сторонние плагины запускают чужой код от вашего имени и получают доступ EdgeEver, в том числе чтение и изменение заметок и подключение к интернету. Включайте только те плагины, которым доверяете.", + confirm: "Включить сторонние плагины", + }, + toggle: "Включить или отключить {{name}}", + uninstall: "Удалить", + settings: { + title: "Настройки плагина", + open: "Открыть настройки {{name}}", + loadFailed: "Не удалось загрузить настройки плагина: {{message}}", + retry: "Перезагрузить настройки", + saveFailed: "Сохранение не завершилось. Проверьте настройки и попробуйте снова: {{message}}", + saved: "Настройки сохранены на этом устройстве.", + required: "Поле «{{name}}» обязательно.", + invalid: "«{{name}}» пока нельзя сохранить.", + secretConfigured: "Задано — оставьте пустым, чтобы не менять", + none: "Нет", + viewList: "Показать список", + listCount: "{{count}} элем.", + }, + details: { + title: "Сведения о плагине", + overview: "Обзор", + navigation: "Навигация по сведениям о плагине", + open: "Сведения о {{name}}", + notFound: "Плагин не найден. Возможно, он удалён.", + type: "Тип", + version: "Версия", + source: "Источник", + installedAt: "Установлен", + networkHosts: "Объявленные сайты (устаревшие метаданные)", + }, + updates: { + check: "Проверить обновления", + found: "Найдено обновлений: {{count}}", + upToDate: "Актуально", + checkFailed: "Не удалось проверить обновления: {{message}}", + available: "Обновить до v{{version}}", + update: "Обновить", + updating: "Обновление…", + confirmTitle: "Обновить {{name}}?", + confirmDescription: "EdgeEver никогда не устанавливает обновления автоматически. Подтвердите загрузку и замену текущей версии с сохранением состояния включения.", + addedAccessDescription: "В этой версии меняются объявленные возможности или устаревшие сетевые метаданные. Включённые плагины всегда имеют полный доступ к API плагинов — обновляйте только из доверенного источника.", + addedPermissions: "Новые объявления возможностей", + addedNetworkHosts: "Новые объявления сайтов (устаревшие метаданные)", + allowAndUpdate: "Подтвердить и обновить", + }, + noticeTitle: "Плагин", + panel: "Панель плагина", + panelDescription: "Этот интерфейс предоставлен включённым доверенным плагином.", + sources: { + verified: "Проверено", + community: "Сообщество", + github: "Установка с GitHub", + manifest: "Установка по манифесту", + marketplace: "Установка из магазина", + }, + marketplace: { + title: "Магазин плагинов", + refresh: "Обновить магазин плагинов", + loading: "Загрузка магазина плагинов…", + loadFailed: "Не удалось загрузить магазин плагинов: {{message}}", + retry: "Повторить", + officialAutoUpdate: "Официальный · автообновления", + installVerified: "Заменить проверенной версией", + openRepository: "Открыть исходный репозиторий {{name}}", + license: "Лицензия {{spdx}}", + admissionNotice: "Проверка допуска пройдена. Это не гарантия безопасности.", + revoked: "Плагин удалён из каталога: {{reason}}. Отключите его.", + submissionTitle: "Отправить плагин сообщества", + submissionDocs: "Репозиторий плагинов сообщества", + submissionRepository: "URL репозитория GitHub для отправки", + precheck: "Предпроверка", + precheckRunning: "Предпроверка…", + precheckPassed: "Предпроверка пройдена. Добавьте plugins/{{id}}.json в репозиторий сообщества с тем же URL и ID плагина.", + openSubmission: "Открыть инструкцию по отправке", + }, + toolbar: { + open: "Плагины", + empty: "Включённые плагины пока не предоставили команд или панелей.", + manage: "Управление плагинами и темами", + }, + schedules: { + legacyTitle: "Устаревшие ручные задачи", + legacyDescription: "Эти задачи созданы вручную в более ранней версии. Новые расписания настраивайте в параметрах соответствующего плагина; здесь по-прежнему можно отключить или удалить устаревшие задачи.", + toggle: "Включить или отключить {{name}}", + delete: "Удалить {{name}}", + history: "История запусков", + historyFor: "История запусков: {{name}}", + historyTitle: "{{name}} · История запусков", + historyDescription: "{{cron}} · {{timezone}}", + status: { + running: "Выполняется", + succeeded: "Успешно", + failed: "Ошибка", + }, + }, + }, + executionHistory: { + centerTitle: "Журналы запланированных задач", + centerDescription: "Статус, длительность и сведения об ошибках системных фоновых задач.", + openCenter: "Журналы запланированных задач", + pluginSchedule: "Плагин {{plugin}} · {{key}}", + pagination: "Постраничная навигация истории запусков", + range: "{{start}}–{{end}} из {{total}}", + page: "Страница {{page}} из {{total}}", + previousPage: "Назад", + nextPage: "Далее", + retry: "Повторить", + loading: "Загрузка", + loadFailed: "Не удалось загрузить историю запусков.", + empty: "Запусков пока нет.", + total: "{{count}} запусков", + loadMore: "Загрузить ещё", + currentDevice: "Текущее устройство", + device: "Устройство ···{{id}}", + scheduledFor: "Запланировано на", + startedAt: "Начало", + duration: "Длительность", + running: "В процессе", + status: { + running: "Выполняется", + succeeded: "Успешно", + failed: "Ошибка", + }, + }, + objectStorage: { + title: "Объектное хранилище OSS", + description: "Настройте объектное хранилище для новых изображений и вложений в этом экземпляре EdgeEver. Изменения может вносить только владелец экземпляра.", + providers: { + builtin: { title: "Встроенное хранилище", description: "Использовать привязку Cloudflare R2 или хранилище по умолчанию текущей среды выполнения." }, + s3: { title: "Сторонний OSS", description: "Подключите S3-совместимый сервис OSS, COS, S3, MinIO или R2." }, + }, + displayName: "Имя конфигурации", + endpoint: "Эндпоинт", + region: "Регион", + bucket: "Бакет", + accessKeyId: "Ключ доступа (Access Key ID)", + secretAccessKey: "Секретный ключ (Secret Access Key)", + secretSavedHint: "Секрет зашифрован. Оставьте поле пустым, чтобы не менять его.", + objectPrefix: "Префикс объектов", + objectPrefixHint: "Необязательно. Новые объекты сохраняются под этим префиксом.", + pathStyle: "Доступ в стиле Path-style", + pathStyleHint: "Обычно требуется для MinIO и пользовательских endpoint.", + test: "Проверить подключение", + testSucceeded: "Проверки загрузки, чтения и удаления пройдены.", + saved: "Настройки объектного хранилища сохранены.", + failed: "Операция с объектным хранилищем не удалась.", + authenticationRequired: "Перед сохранением учётных данных стороннего хранилища задайте EDGE_EVER_AUTH_PASSWORD в среде развёртывания. В Docker этот секрет хранится на томе данных, чтобы не потерять его при последующих обновлениях.", + switchHint: "Переключение влияет только на новые загрузки. Существующие вложения остаются в исходном хранилище и не переносятся автоматически.", + demoDisabled: "На демо-экземпляре объектное хранилище изменить нельзя.", + }, + aiModel: { + title: "Внешние модели ИИ", + provider: "Протокол API", + providers: { + "openai-compatible": "Совместимый с OpenAI", + anthropic: "Anthropic Messages", + google: "Google Gemini", + }, + displayName: "Имя сервиса", + baseUrl: "Базовый URL", + modelId: "ID модели", + modelIdPlaceholder: "Введите ID модели или сначала найдите доступные модели", + apiKey: "API-ключ", + apiKeyCreateHint: "Учётные данные шифруются на сервере EdgeEver.", + apiKeySavedHint: "Ключ зашифрован. Оставьте поле пустым, чтобы не менять его.", + apiKeyStoredPlaceholder: "Сохранено; введите новый ключ, чтобы заменить", + serviceEnabled: "Включить сервис", + serviceEnabledStatus: "Включено", + serviceDisabledStatus: "Отключено", + serviceActions: "Действия с сервисом", + defaultSettingsTitle: "Настройки использования", + defaultModel: "Модель по умолчанию для ИИ заметок", + noDefaultModel: "Модель не выбрана", + defaultUnavailable: "Сервис текущей модели по умолчанию отключён. Включите его или выберите другую модель.", + noProviders: "Пока нет сервисов моделей. Добавьте OpenAI, OpenRouter, Anthropic, Gemini или другой совместимый сервис, чтобы начать.", + servicesTitle: "Сервисы моделей", + defaultProviderName: "Провайдер моделей {{ordinal}}", + addProvider: "Добавить сервис моделей", + createProvider: "Добавить сервис", + initialModelId: "Начальный ID модели (необязательно)", + deleteProviderConfirm: "Удалить «{{name}}» и все его модели?", + editConnection: "Изменить подключение", + connectionDescription: "Измените endpoint и учётные данные для «{{name}}». Проверка использует текущую форму без сохранения.", + modelsTitle: "Доступные модели", + modelCount: "Модели: {{count}}", + modelsHint: "Один Base URL может содержать несколько моделей. Например, один сервис OpenRouter может предоставлять модели от разных поставщиков.", + noModels: "Модели ещё не добавлены. Найдите доступные модели или введите ID модели вручную.", + discoverModels: "Найти модели", + refreshModels: "Обновить список моделей", + discoveryLoading: "Загрузка доступных моделей…", + discoveryFailed: "Не удалось загрузить список моделей. Можно использовать ранее загруженные или ввести ID модели вручную и повторить обновление.", + discoveryComplete: "Найдено доступных моделей: {{count}}.", + addModel: "Добавить модель", + addModelDescription: "Введите ID модели вручную или сначала найдите модели, доступные у этого сервиса.", + defaultBadge: "По умолчанию", + deleteModelConfirm: "Удалить модель «{{name}}»?", + removeModel: "Удалить модель", + test: "Проверить подключение", + testSucceeded: "Endpoint модели ответил успешно.", + saved: "Сервис моделей сохранён.", + failed: "Операция с моделью ИИ не выполнена.", + encryptionKeyMissing: "Этот экземпляр не может шифровать учётные данные ИИ. Задайте EDGE_EVER_AUTH_PASSWORD или необязательный EDGE_EVER_CREDENTIALS_ENCRYPTION_KEY в среде развёртывания и перезапустите. Docker хранит секрет на томе данных, поэтому при последующих обновлениях он не потеряется.", + addDisabledReason: "Настройте секрет аутентификации экземпляра перед добавлением сервиса моделей", + savedCredentialsUnavailable: "Сохранённые учётные данные модели не удаётся расшифровать. Откройте сервис и введите API-ключ снова.", + savedCredentialsUnavailableBadge: "Ввести API-ключ снова", + }, + speechTranscription: { + title: "Настройки модели распознавания речи", + baseUrl: "URL конечной точки", + baseUrlPlaceholder: "https://api.example.com/v1", + baseUrlHint: "Запросы идут в /audio/transcriptions по этому URL; сервис должен поддерживать эту точку.", + modelId: "ID модели", + modelIdPlaceholder: "whisper-1", + apiToken: "Токен API", + apiTokenHint: "Ключ шифруется на сервере EdgeEver.", + apiTokenSavedHint: "Токен зашифрован. Оставьте поле пустым, чтобы не менять его.", + apiTokenStoredPlaceholder: "Сохранён; введите новый токен, чтобы заменить", + apiTokenRequired: "Чтобы включить расшифровку, нужен токен API.", + saved: "Настройки расшифровки сохранены.", + failed: "Не удалось сохранить настройки расшифровки.", + demoDisabled: "На демо-экземпляре расшифровку изменить нельзя.", + defaultModel: "Модель речи по умолчанию", + noDefaultModel: "Модель не выбрана", + defaultUnavailable: "Сервис текущей модели по умолчанию выключен. Включите его или выберите другую модель.", + defaultBadge: "По умолчанию", + addProvider: "Добавить сервис модели", + noProviders: "Сервисов речи пока нет. Добавьте один, включите переключателем и выберите модель по умолчанию выше.", + displayName: "Название", + standard: "Стандарт совместимости", + standards: { + "openai-compatible": "Совместимый с OpenAI", + }, + serviceEnabled: "Включить сервис", + serviceEnabledStatus: "Включён", + serviceDisabledStatus: "Выключен", + serviceActions: "Действия сервиса", + addModel: "Добавить модель", + editConnection: "Изменить подключение", + test: "Проверить соединение", + testModel: "Модель для проверки", + testModelRequired: "Сначала добавьте речевую модель в этот сервис.", + testHint: "Короткий встроенный звук отправляется с этого клиента напрямую в сервис модели. У провайдера может быть небольшая плата. Проверка форму не сохраняет.", + testSucceeded: "Соединение работает. Расшифровка: «{{transcript}}»", + testFailed: "Проверка сервиса речи не удалась.", + browserDirectUnavailable: "Браузер не смог подключиться к сервису речи напрямую. Провайдер может блокировать межсайтовые запросы (CORS), или сеть недоступна. Токен и модель не проверены. Проверьте в приложении для компьютера или выберите провайдера, который разрешает запросы из браузера.", + desktopDirectUnavailable: "Приложение для компьютера не смогло подключиться к сервису речи. Проверьте адрес и сеть; токен и модель не проверены.", + removeModel: "Удалить модель", + noModels: "Моделей пока нет", + deleteProviderConfirm: "Удалить сервис речи «{{name}}»? Сохранённый токен тоже будет удалён.", + deleteModelConfirm: "Удалить модель «{{name}}»?", + savedCredentialsUnavailable: "Сохранённый токен речи не расшифровывается. Откройте сервис и введите его снова.", + statusOn: "Вкл", + statusOff: "Выкл", + resultTitle: "Расшифровка", + recognizing: "Расшифровка медиа…", + recognizingProgress: "Расшифровка… готово фрагментов: {{count}}.", + recognizeFailed: "Расшифровка не удалась", + copy: "Копировать текст", + copied: "Скопировано", + insertIntoNote: "Вставить в заметку", + retry: "Повторить расшифровку", + attachmentNotice: "Аудио извлекается и режется на этом клиенте, затем по частям отправляется напрямую в выбранный сервис речи.", + }, + aiPrompts: { + title: "Библиотека промптов", + description: "Управляйте повторно используемыми промптами для обработки заметок. В новых рабочих пространствах есть редактируемые стартовые промпты — их можно изменить или удалить.", + nav: "Промпты", + openLibrary: "Открыть библиотеку промптов", + settingsTitle: "Промпты ИИ", + settingsDescription: "Повторно используемые промпты отображаются в ИИ-помощнике. Управляйте ими в библиотеке промптов.", + listTitle: "Все промпты", + listSubtitle: "Стартовые и созданные вами промпты — всё можно редактировать", + empty: "Промптов пока нет. Создайте здесь или восстановите стандартные.", + emptyHint: "Нажмите «Новый промпт» или «Восстановить стандартные», чтобы вернуть стартовые.", + count: "{{count}}", + add: "Новый промпт", + edit: "Редактировать промпт", + create: "Создать", + save: "Сохранить", + delete: "Удалить", + deleteConfirm: "Удалить промпт «{{name}}»? Стартовые промпты можно вернуть через «Восстановить стандартные».", + preview: "Просмотреть промпт", + restoreDefaults: "Восстановить по умолчанию", + restoreDefaultsHint: "Снова добавляет только удалённые стартовые промпты. Изменённые стартовые и созданные вами промпты остаются без изменений.", + restoreDefaultsConfirmTitle: "Восстановить стандартные промпты?", + restoreDefaultsConfirmDescription: "Отсутствующие стартовые промпты (например, «Кратко» и «Перевести») будут добавлены снова. Изменённые стартовые и созданные вами промпты не изменятся.", + restoreDefaultsConfirm: "Восстановить", + restoreDefaultsDone: "Восстановлено стандартных промптов: {{count}}.", + restoreDefaultsNone: "Все стандартные промпты уже есть.", + name: "По имени", + namePlaceholder: "Например: Еженедельный дайджест / Задачи со встречи", + descriptionLabel: "Описание (необязательно)", + descriptionPlaceholder: "Когда использовать этот промпт", + instruction: "Промпт", + instructionPlaceholder: "Например: Извлеки прогресс за неделю, риски и следующие приоритеты в виде списка Markdown.", + parameterKind: "Дополнительный ввод", + parameterKinds: { + none: "Нет", + "target-language": "Целевой язык", + tone: "Тон", + }, + resultMode: "Действия с результатом", + resultModes: { + both: "Добавить или заменить", + append: "Только добавить", + replace: "Только заменить", + }, + defaultBadge: "Стартовый", + saved: "Промпт сохранён.", + deleted: "Промпт удалён.", + failed: "Операция с промптом не выполнена.", + demoDisabled: "В демо-режиме промпты нельзя изменять.", + }, + aiAssistant: { + title: "ИИ-помощник для заметок", + modes: { + instruction: "Вопросы и ответы", + ask: "Агент", + askHint: "Ищите и организуйте заметки. Удалённые заметки попадают в Корзину; правки можно восстановить из истории.", + askPlaceholder: "Спросите заметки или попросите агента их организовать…", + resumeLast: "Продолжить последнее", + }, + open: "Открыть ИИ-помощник для письма", + openForSelection: "Спросить ИИ", + actionLabel: "Действие", + selectedScope: "Выделенный текст", + workspaceScope: "Все заметки", + noteScope: "Вся заметка", + noteScopeHint: "ИИ использует текущую заметку как контекст.", + selectedActions: "Изменить выделенный текст", + noteActions: "Обработать всю заметку", + myPrompts: "Мои промпты", + useCustom: "Свой промпт", + useCustomHint: "Напишите свой запрос на обработку", + transparentHint: "Текст ниже — то, что получит ИИ: те же промпты, что в библиотеке, полностью редактируемые.", + managePrompts: "Библиотека промптов", + actions: { + summarize: "Кратко", + "extract-key-points": "Ключевые мысли", + "extract-todos": "Выделить задачи", + "rewrite-proofread": "Переписать и вычитать", + translate: "Перевести", + "improve-writing": "Улучшить стиль", + "fix-spelling-grammar": "Исправить орфографию и грамматику", + "make-shorter": "Сделать короче", + "make-longer": "Сделать длиннее", + "simplify-language": "Упростить язык", + "change-tone": "Изменить тон", + "continue-writing": "Продолжить текст", + custom: "Свой промпт", + }, + targetLanguage: "Язык перевода", + targetLanguages: { + en: "Английский", + "zh-CN": "Упрощённый китайский", + "zh-TW": "Традиционный китайский", + ja: "Японский", + ko: "Корейский", + es: "Испанский", + fr: "Французский", + de: "Немецкий", + pt: "Португальский", + pl: "Польский", + }, + tone: "Тон", + tones: { + professional: "Деловой", + friendly: "Дружелюбный", + casual: "Неформальный", + direct: "Прямой", + }, + customInstruction: "Скажите ИИ, что сделать", + customInstructionPlaceholder: "Например: Перепиши это как краткое дружелюбное письмо и сохрани все даты.", + customInstructionSelected: "Скажите ИИ, что сделать с выделенным фрагментом", + customInstructionSelectedPlaceholder: "Например: Сделай текст более разговорным, сохрани термины и не меняй структуру.", + customInstructionRequired: "Введите инструкцию для ИИ.", + inputContent: "Контент для обработки", + inputContentPlaceholder: "Введите контент или оставьте поле пустым, чтобы обработать всю заметку.", + inputContentEmptyPlaceholder: "Введите или вставьте контент для обработки.", + inputScope: "Входной контент", + addAttachment: "Добавить файлы", + attachments: "Прикреплённые файлы", + removeAttachment: "Удалить {{name}}", + attachmentHint: "До 4 изображений, PDF или текстовых файлов. Файлы используются только для этого запроса к ИИ.", + attachmentErrors: { + count: "Можно прикрепить до 4 файлов.", + fileTooLarge: "Файл слишком большой. Текстовые файлы — до 256 КБ; изображения и PDF — до 4 МБ.", + totalTooLarge: "Общий размер вложений — до 8 МБ.", + unsupported: "Этот тип файла не поддерживается. Выберите изображение, PDF, TXT, Markdown, CSV или JSON.", + readFailed: "Не удалось прочитать файл. Попробуйте выбрать его снова.", + }, + saveAsPrompt: "Сохранить как промпт", + saveAsPromptTitle: "Сохранить как промпт", + updatePrompt: "Обновить этот промпт", + promptName: "Название промпта", + promptNamePlaceholder: "Например: Еженедельный дайджест", + promptDescription: "Описание (необязательно)", + promptSaved: "Промпт сохранён.", + promptMissing: "Выбранный промпт больше не существует. Выберите другой.", + promptUpdated: "Промпт обновлён.", + promptSaveFailed: "Не удалось сохранить промпт.", + generate: "Сгенерировать", + generating: "Генерация…", + stop: "Остановить", + result: "Результат генерации", + resultPlaceholder: "Результат генерации появится здесь.", + copy: "Копировать результат", + copied: "Результат скопирован.", + discard: "Очистить результат", + retry: "Повторить", + refine: "Продолжить доработку", + refinePlaceholder: "Например: Сделай короче, используй маркеры или более естественный тон", + refineAction: "Доработать", + append: "Добавить в конец заметки", + replace: "Заменить заметку", + replaceSelection: "Принять и заменить выделение", + applyFailed: "Не удалось применить результат генерации. Выделите контент снова и повторите попытку.", + sourceRequired: "Для этого действия нужен контент заметки. Сначала добавьте текст или используйте свой промпт для генерации из пустой заметки.", + requestInvalid: "Запрос к ИИ некорректен. Проверьте выбранное действие и ввод, затем попробуйте снова.", + networkError: "Не удалось подключиться к сервису. Убедитесь, что он запущен, и попробуйте снова.", + configure: "Сначала настройте модель ИИ в разделе «Я → Интеграции ИИ».", + sidebar: { + title: "ИИ-ассистент", + close: "Свернуть ассистента", + history: "История чатов", + newThread: "Новый чат", + currentInfographic: "Эта инфографика", + historyRecent: "Последние 30 дней", + historyOlder: "Ранее", + historySearch: "Поиск чатов", + historyEmpty: "Нет подходящих чатов", + resize: "Изменить ширину ассистента", + stop: "Остановить", + placeholder: "Спросить ИИ", + emptyTitle: "Начните разговор", + includeCurrentNote: "Включить текущую заметку", + working: "Ответ…", + selectionBadge: "Выбрано символов: {{count}} · текущая заметка", + selection: { + explain: "Объяснить", + translate: "Перевести", + ask: "Спросить", + actions: "Выделенный текст", + explainPrompt: "Объясни мысли в отрывке ниже:", + translatePrompt: "Переведи отрывок ниже на язык этого разговора. Если исходный текст уже на этом языке и целевой язык не задан, спроси только, на какой другой язык перевести; иначе выведи только перевод.", + translateLanguagePrompt: "Переведи отрывок ниже на {{language}}. Напиши только перевод, без предисловия.", + truncated: "Отправлен только первый фрагмент выделения: {{count}} символов.", + dismiss: "Открепить", + copy: "Копировать", + copied: "Скопировано", + replace: "Заменить выделенный текст", + stale: "Заметка изменилась, поэтому текст не записан.", + chips: { + "zh-CN": "中文", + en: "Английский", + ja: "日本語", + pl: "Польский", + }, + languageNames: { + "zh-CN": "Упрощённый китайский", + en: "английский", + ja: "японский", + pl: "польский", + }, + }, + skills: { + summarize: "Кратко", + improve: "Улучшить текст", + translate: "Перевести", + }, + skillPrompts: { + summarize: "Кратко перескажи то, что я сейчас смотрю, и ответь этим пересказом. Не утверждай, что заметка уже изменена. Предлагай правку заметки, только если я хочу её изменить.", + improve: "Улучши текст того, что я сейчас смотрю, и ответь исправленным текстом. Не утверждай, что заметка уже изменена. Предлагай правку заметки, только если я хочу её изменить.", + translate: "Переведи то, что я сейчас смотрю, и ответь переводом. Не утверждай, что заметка уже изменена. Предлагай правку заметки, только если я хочу её изменить.", + }, + slash: { + summarize: "Кратко пересказать текущий текст", + improve: "Улучшить текущий текст", + translate: "Перевести текущий текст", + }, + attachmentUnsupported: "Эта модель не поддерживает такие вложения. Они остаются в поле ввода.", + attachmentExpired: "Срок этих вложений истёк. Они остаются в поле ввода — отправьте их снова.", + localRejected: "Этот локальный агент не поддерживает эти файлы. Они остаются в поле ввода: {{names}}", + localStatus: "Локальный агент", + localMissing: "Локальный агент недоступен. Выберите адаптер или встроенного ассистента.", + noteAccessUnavailable: "Не удалось подключиться к заметкам этого аккаунта. Проверьте подключение к экземпляру и вход, затем повторите.", + noteAccessNetwork: "Сейчас не удаётся подключиться к заметкам. Проверьте сеть или сервис экземпляра и повторите.", + noteAccessLoginRequired: "Сессия EdgeEver недоступна или истекла. Войдите в текущий экземпляр снова и повторите.", + noteAccessChanged: "Экземпляр или аккаунт изменились, запрос остановлен. Повторите с текущим аккаунтом.", + localLoginRequired: "Этому локальному агенту нужен вход. Войдите в Настройки → Модели и агенты и повторите.", + workbuddyLoginRequired: "WorkBuddy ACP не смог использовать сохранённый вход. Выберите способ входа ACP в Настройки → Модели и агенты и повторите.", + agentRefused: "Локальный агент отклонил запрос. Проверьте вход или настройки модели и повторите.", + unavailable: "Чтобы отправить сообщение, войдите в личный аккаунт. В демо и без входа встроенный ассистент недоступен.", + proposalConfirm: "Применить к заметке", + proposalReject: "Не применять", + proposalConflict: "Заметка изменилась, поэтому ничего не применено.", + proposalConfirmOther: "Подтвердить", + proposalDismiss: "Закрыть", + toolProgress: "{{name}} · {{status}}", + processTools: { + activity: "Работа инструментов", + webSearch: "Поиск в интернете", + imageGeneration: "Генерация изображения", + command: "Выполнить команду", + other: "Вызов инструмента", + pending: "Ожидание", + running: "Выполняется", + completed: "Готово", + failed: "Не завершено", + raw: "Журнал", + }, + generatedImage: "Сгенерированное изображение", + downloadImage: "Скачать изображение", + imageSaveFailed: "Изображение не удалось сохранить локально. Скачайте его сейчас: после закрытия страницы оно может пропасть.", + }, + agentSource: { + configure: "Настроить модели и агентов", + switchNoteAgentHint: "Выбранный агент будет править эту инфографику дальше.", + optionUnavailable: "Недоступно", + + switch: "Сменить ИИ", + newAgentThread: "Переход к другому агенту начинает новый чат. Прежние чаты остаются в истории.", + noModels: "Настройте модели ИИ в параметрах.", + switchUnavailable: "Агент недоступен. Проверьте подключение в настройках.", + + title: "Режим агента", + description: "Используйте встроенного агента или подключитесь к агенту (ACP) на этом компьютере.", + localDisabled: "Нужно приложение для компьютера.", + builtin: "Встроенный агент", + builtinHint: "Использует сервисы моделей и инструменты, настроенные ниже.", + local: "Локальный агент (ACP)", + localHint: "Подключается к локальному агенту по ACP и автоматически даёт чтение и запись заметок текущего аккаунта.", + adapter: "Агент", + codex: "Codex", + antigravity: "Antigravity", + claudeCode: "Claude Code", + openClaw: "OpenClaw", + hermesAgent: "Hermes Agent", + workbuddy: "WorkBuddy", + workbuddyCn: "WorkBuddy Китай", + workbuddyIntl: "WorkBuddy международный", + grokBuild: "Grok Build", + deepseekHarness: "DeepSeek Harness", + piAgent: "pi agent", + pathLabel: "Исполняемый файл Antigravity", + pathPlaceholder: "/absolute/path/to/antigravity-acp", + choosePath: "Выбрать файл", + pathHint: "Выберите существующий исполняемый файл Antigravity ACP или установите коннектор автоматически.", + install: "Установить коннектор", + installHint: "Если локальный агент найден, его коннектор скачивается из официального источника. Размер может превысить 100 МиБ; обновления проверяются автоматически.", + claudeCodeNotFound: "Коннектор Claude Code ACP не найден", + claudeCodeCredentialsRequired: "Нет входа / нет учётных данных", + claudeCodeLoginHint: "Нет учётных данных Claude Code. Войдите в Claude Code в терминале или укажите шлюз в ~/.claude/settings.json; приложения из Dock не читают ~/.zshrc. Для своего шлюза задайте ANTHROPIC_BASE_URL и полный ID модели opus в ANTHROPIC_DEFAULT_OPUS_MODEL, затем проверьте соединение снова.", + identityStatusNotReported: "Статус входа не сообщён", + identityStatusUnavailable: "Агент не сообщил статус входа. Проверьте его настройки и desktop.log, затем повторите.", + claudeCodeMissingHint: "Нажмите «Установить коннектор», чтобы EdgeEver установил его и проверил соединение.", + openClawNotFound: "OpenClaw CLI не найден", + openClawMissingHint: "Установите OpenClaw CLI и запустите его Gateway, затем проверьте соединение.", + openClawNoteAccessHint: "Чтобы открыть заметки, настройте EdgeEver MCP в OpenClaw Gateway и подтвердите экземпляр и аккаунт.", + hermesAgentNotFound: "Hermes Agent CLI не найден", + hermesAgentMissingHint: "Установите Hermes Agent с дополнением ACP и убедитесь, что hermes acp --check завершается успешно.", + hermesAgentSetupHint: "Настройте провайдера модели и войдите через Hermes (например, hermes model), затем проверьте соединение снова.", + grokBuildMissingHint: "В Grok Build есть ACP. Сначала установите Grok CLI; EdgeEver использует локальную команду grok без отдельной загрузки коннектора.", + grokBuildNotFound: "Grok CLI не найден", + deepseekHarnessNotFound: "DeepSeek Harness CLI не найден", + deepseekHarnessMissingHint: "Установите DeepSeek Harness CLI (dsh). EdgeEver использует официальную точку dsh --profile acp; само приложение для компьютера этот CLI не содержит.", + piAgentNotFound: "pi agent не найден", + piAgentAdapterMissing: "Коннектор pi ACP не найден", + piAgentMissingHint: "Сначала установите pi agent. Когда он будет найден, EdgeEver установит и обновит коннектор pi-acp по запросу.", + piAgentAdapterMissingHint: "pi agent установлен. EdgeEver ставит pi-acp сам, или можно нажать «Установить коннектор» сейчас.", + piAgentLoginHint: "Запустите pi в терминале, войдите к провайдеру модели или укажите ключ API, затем проверьте соединение снова.", + workbuddyNotFound: "Программа WorkBuddy ACP не найдена", + workbuddyMissingHint: "В приложении WorkBuddy для компьютера уже есть программа ACP; отдельный CodeBuddy не нужен. Отдельный CodeBuddy CLI тоже подойдёт.", + workbuddyCnLoginHint: "Войдите в WorkBuddy и проверьте соединение. Используйте WeChat или корпоративный вход.", + workbuddyIntlLoginHint: "Войдите в WorkBuddy и проверьте соединение. Используйте Google / GitHub или корпоративный вход.", + workbuddyCredentialsUnavailable: "Сохранённый вход WorkBuddy прочитать не удалось, ACP недоступен. Пока используйте другого локального агента; повторный вход это не исправит.", + workbuddyConnectedHint: "Можно начать чат с аккаунтом, под которым выполнен вход в WorkBuddy.", + workbuddyLoginOptions: "Способы входа", + workbuddyLoginRequired: "Войдите в WorkBuddy", + workbuddyAuthenticateWith: "Войти через {{method}}", + installing: "Установка и проверка…", + installFailed: "Установка или проверка соединения не удались. Предыдущая версия сохранена.", + updateFailed: "Автообновление не удалось. Используется предыдущая версия.", + updateFailureReason: "Причина: {{reason}}", + connectionFailureReason: "Причина сбоя соединения: {{reason}}", + connectionFailureUnknown: "Приложение не вернуло конкретную причину. Проверьте соединение снова и приложите диагностический журнал.", + updateTimeoutHint: "Загрузка или соединение превысили время ожидания. Проверьте сеть и повторите.", + updateNetworkHint: "Сервис обновлений недоступен. Проверьте сеть и системный прокси, затем повторите.", + retryUpdate: "Повторить обновление", + updating: "Обновление…", + authenticating: "Вход…", + authenticationTimedOut: "Вход не завершился. Повторите попытку.", + authenticationFailed: "Вход не удался или был отменён. Повторите.", + authenticateWith: "Войти через {{method}}", + authAvailableHint: "У этого агента есть вход ACP. Если для сообщения нужен вход, выполните его здесь.", + loginUnavailable: "Этому коннектору ACP нужна авторизация, но способа входа ACP нет. Войдите в самом агенте и проверьте соединение снова.", + autoUpdated: "Обновлено автоматически", + probe: "Проверить соединение", + probing: "Проверка…", + probeHint: "Открывает и закрывает временную сессию, не отправляя ваши заметки. Вход в модель эта проверка не подтверждает.", + notProbed: "Ещё не проверено", + invalidPath: "Укажите абсолютный путь к исполняемому файлу.", + states: { + not_probed: "Ещё не проверено", + not_installed: "Коннектор ACP не найден", + installing: "Автоматическая установка коннектора ACP…", + needs_login: "Нужен вход", + available: "Сессия доступна", + failed: "Соединение не установлено", + }, + }, + }, + accountInfo: { + title: "Текущий аккаунт", + }, + systemInfo: { + title: "Сведения о системе", + description: "Просмотр версии и сведений о среде выполнения облачного инстанса и текущего клиента.", + cloudSection: "Облачный инстанс", + cloudSectionDescription: "Версия и среда развёртывания подключённого инстанса.", + clientSection: "Текущий клиент", + clientSectionDescription: "Приложение EdgeEver и среда выполнения на этом устройстве.", + connectionSection: "Подключение и синхронизация", + connectionSectionDescription: "Статус подключения между этим клиентом и облачным инстансом.", + version: "Версия", + instanceUrl: "URL инстанса", + instanceVersion: "Версия инстанса", + instanceBuild: "Сборка инстанса", + deploymentVersionTime: "Время развёртывания экземпляра", + databaseMigration: "Версия базы данных", + databaseBackend: "Бэкенд базы данных", + databaseBackends: { + d1: "D1", + sqlite: "SQLite", + }, + newUploadObjectStorage: "Объектное хранилище для новых загрузок", + objectStorageBackends: { + builtin_r2: "Встроенный R2", + builtin_filesystem: "Локальная файловая система", + builtin_s3: "S3-совместимое хранилище инстанса", + external_s3: "Стороннее S3-совместимое OSS", + }, + existingAttachments: "Существующие вложения", + existingAttachmentsOriginalStorage: "Продолжать читать из исходного хранилища", + clientVersion: "Версия клиента", + releaseTime: "Время выпуска", + build: "Сборка", + client: "Клиент", + clients: { + desktopApp: "Десктопное приложение", + mobileApp: "Мобильное приложение", + pwa: "PWA", + web: "Веб", + }, + deploymentPlatform: "Платформа развёртывания", + deploymentPlatforms: { + cloudflare: "Cloudflare", + docker: "Docker", + unknown: "Неизвестно", + }, + containerImageSource: "Источник образа контейнера", + imageBuiltAt: "Образ собран", + imageBuildSource: "Источник сборки образа", + containerImageSources: { + "official-ghcr": "GitHub Container Registry (GHCR)", + "official-cn-mirror": "Официальное зеркало для материкового Китая", + custom: "Пользовательский образ", + unknown: "Неизвестно", + }, + deployment: "Источник развёртывания", + deploymentTriggers: { + github_release: "Триггер GitHub Release", + main_push: "Триггер push в main", + manual: "Ручной триггер", + unknown: "Неизвестный триггер", + }, + deploymentMethods: { + github_actions: "через GitHub Actions", + cloudflare_workers_builds: "через Cloudflare Workers Builds", + local_cli: "через локальный CLI", + docker: "через Docker", + unknown: "Неизвестный метод", + }, + browser: "Браузер", + os: "Сведения о системе", + architecture: "Архитектура", + runtimeEngine: "Движок выполнения", + language: "Язык", + timeZone: "Часовой пояс", + screenResolution: "Разрешение экрана", + screenResolutionValue: "{{screen}} @{{dpr}}x", + deviceModel: "Модель устройства", + instanceConnection: "Подключение к инстансу", + requestLatency: "Время проверки состояния", + lastSuccessfulSync: "Последняя успешная синхронизация", + pendingSync: "Ожидает синхронизации", + failedSync: "Ошибка или конфликт", + connectionConnected: "Подключено", + connectionChecking: "Проверка", + connectionFailed: "Ошибка подключения", + neverSynced: "Ещё не синхронизировано", + nothingToSync: "Нечего синхронизировать", + installMode: "Режим", + standalone: "Установленное приложение", + browserMode: "Браузер", + unknown: "Неизвестно", + copy: "Копировать сведения", + copySucceeded: "Сведения о системе скопированы в буфер обмена.", + copyFailed: "Не удалось скопировать. Проверьте разрешения буфера обмена и попробуйте снова.", + copyFieldLabel: "Поле", + copyValueLabel: "Значение", + clientAheadOfInstanceByPlatform: { + cloudflare: "Этот клиент новее подключённого облачного экземпляра. Можно дождаться ежедневного автоматического обновления экземпляра или запустить workflow «Update deployed EdgeEver».", + docker: "Этот клиент новее подключённого облачного экземпляра. Можно дождаться ежедневного автоматического обновления экземпляра или выполнить ./update.sh в каталоге установки (по умолчанию ~/edgeever).", + unknown: "Этот клиент новее подключённого облачного экземпляра. Можно дождаться ежедневного автоматического обновления экземпляра или обновить экземпляр вручную.", + }, + deployedUpdateTitle: "Ваш облачный экземпляр автоматически обновлён до v{{version}}", + viewReleaseNotes: "Заметки о выпуске", + desktopCheckForUpdates: "Проверить обновления", + desktopDownloadLatest: "Скачать последний AppImage", + desktopUpdateChecking: "Проверка обновлений…", + desktopUpdateCurrent: "У вас актуальная версия.", + desktopUpdateVerifying: "Проверка обновления…", + desktopUpdateDownloading: "Загрузка обновления…", + desktopUpdateReady: "Обновление загружено", + desktopUpdateDownload: "Скачать обновление", + desktopUpdateRestart: "Перезапустить для обновления", + desktopUpdateInstalling: "Перезапуск для установки обновления…", + desktopUpdateDiagnosticDetails: "Диагностика", + desktopUpdateDiagnosticCopied: "Диагностика скопирована", + desktopUpdateManualDownload: "Скачать последнюю версию с GitHub", + desktopCopyUpdateDiagnostic: "Копировать диагностику обновления", + desktopUpdateDiagnosticHint: "Скопируйте эти сведения, когда сообщаете о проблеме. «at» — время UTC, localTime — часовой пояс устройства.", + desktopUpdateFailed: "Не удалось обновить. Попробуйте позже.", + }, + feedback: { + title: "Обратная связь", + description: "Сообщить о проблеме или предложить функцию", + issueTitlePrefix: "[Обратная связь] ", + issueContentHeading: "Обратная связь", + issueContentPrompt: "Опишите проблему, шаги для её воспроизведения или функцию, которую хотели бы видеть.", + systemInfoHeading: "Сведения о системе", + systemInfoNotice: "Следующие сведения сформированы EdgeEver для диагностики проблемы.", + privacyNotice: "GitHub Issues общедоступны. Не указывайте пароли, токены, URL экземпляров или содержимое личных заметок.", + }, + dataExport: { + title: "Импорт и экспорт", + description: "Экспортируйте полный ZIP-архив с заметками Markdown, вложениями и историей для резервного копирования или переноса.", + exportButton: "Экспорт ZIP EdgeEver", + importButton: "Импорт ZIP EdgeEver", + working: "Подготовка заметок и вложений…", + importing: "Импорт заметок и вложений…", + progress: "{{completed}} / {{total}}", + scanning: "Проверка структуры ZIP…", + scanProgress: "{{percentage}}%", + complete: "Экспорт завершён. Загрузка ZIP началась.", + error: "Не удалось экспортировать. Проверьте подключение и попробуйте снова.", + largeBackupRequiresStreaming: "Эта резервная копия слишком велика для экспорта в памяти. Используйте Chrome, Edge или приложение для ПК, чтобы сохранить её потоком.", + desktopLocalTitle: "Локальные резервные копии на ПК", + desktopLocalDescription: "Sidecar хранит пять последних резервных копий в локальном каталоге данных SQLite. Перед каждым восстановлением создаётся защитная копия.", + desktopLocalBackup: "Создать резервную копию", + desktopLocalRestore: "Восстановить выбранную копию", + desktopLocalRefreshing: "Чтение списка резервных копий…", + desktopLocalEmpty: "Локальных резервных копий пока нет", + desktopLocalSuccess: "Операция с локальной резервной копией завершена.", + desktopLocalRestoreConfirm: "Восстановление заменит текущие данные на ПК и перезагрузит рабочую область. Продолжить?", + desktopLocalError: "Не удалось выполнить операцию с локальной резервной копией. Проверьте диагностический журнал приложения для ПК.", + desktopLocalProtectedBackup: "Защитная копия перед восстановлением: {{name}}", + webdavTitle: "Резервное копирование WebDAV", + webdavDescription: "Загрузите полный ZIP с заметками, вложениями и историей правок в хранилище WebDAV.", + webdavManualDescription: "Вручную загрузите полный ZIP с заметками, вложениями и историей правок в хранилище WebDAV.", + webdavUrl: "URL WebDAV", + webdavPath: "Удалённая папка резервных копий", + webdavUsername: "Имя пользователя", + webdavPassword: "Пароль", + webdavSecurityNote: "При включённом автоматическом резервном копировании учётные данные WebDAV сохраняются в этом браузере; пароль никогда не отправляется на сервер EdgeEver. Автоматические проверки выполняются, пока открыта страница EdgeEver или PWA; браузеры не гарантируют выполнение, когда приложение полностью закрыто. Служба WebDAV должна разрешать CORS с этого домена.", + webdavManualSecurityNote: "URL и имя пользователя сохраняются в этом браузере; пароль используется только для текущей ручной операции и никогда не отправляется на сервер EdgeEver. Служба WebDAV должна разрешать CORS с этого домена.", + webdavEnableAuto: "Включить автоматическое резервное копирование", + webdavInterval: "Частота резервного копирования", + webdavIntervals: { daily: "Ежедневно", weekly: "Еженедельно (по умолчанию)", biweekly: "Раз в две недели", monthly: "Ежемесячно" }, + webdavLastSuccess: "Последнее успешное автоматическое резервное копирование: {{time}}", + webdavWaitingForFirstBackup: "Автоматическое резервное копирование включено и будет выполняться по расписанию после сохранения настроек.", + webdavTest: "Проверить подключение", + webdavBackup: "Резервная копия в WebDAV", + webdavWorking: "Создание и загрузка резервной копии WebDAV…", + webdavTestComplete: "Подключение к WebDAV выполнено.", + webdavComplete: "Резервная копия загружена: {{filename}}", + webdavErrors: { + network: "Не удалось подключиться к службе WebDAV. Проверьте URL, CORS и сетевое подключение.", + largeBackupRequiresStreaming: "Эта резервная копия слишком велика для этого браузера. Для резервного копирования WebDAV с записью на диск используйте Chrome, Edge или приложение для ПК.", + unknown: "Операция WebDAV не выполнена. Проверьте настройки и попробуйте снова.", + }, + importComplete: "ZIP EdgeEver успешно импортирован.", + importErrors: { + invalidZip: "Не удалось распаковать файл. Это недействительный ZIP или файл повреждён.", + missingManifest: "Не удалось определить файл: отсутствует manifest.json.", + unsupportedFormat: "Формат архива не поддерживается. Выберите ZIP, экспортированный из EdgeEver.", + unsupportedVersion: "Этот ZIP EdgeEver требует более новую версию EdgeEver. Обновите приложение перед импортом.", + invalidManifest: "manifest.json недействителен, поэтому версию и целостность архива проверить нельзя.", + missingData: "В архиве отсутствуют структурированные данные блокнота или заметок.", + invalidData: "Архив содержит недействительные структурированные данные и мог быть изменён или повреждён.", + incompleteData: "Заметки или история правок неполны и не соответствуют манифесту.", + incompleteResources: "Изображения или вложения отсутствуют либо повреждены и не соответствуют манифесту.", + serverRejected: "Сервер не смог записать импортированные данные: {{message}}", + network: "Во время импорта не удалось связаться с EdgeEver. Проверьте подключение и попробуйте снова.", + unknown: "Импорт не выполнен из‑за неизвестной ошибки. Экспортируйте новый архив и попробуйте снова.", + }, + importConfirmTitle: "Импорт ZIP EdgeEver", + importConfirmDescription: "В этом файле {{memos}} заметок и {{resources}} ресурсов. Остальные данные экземпляра сохраняются, но совпадающие идентификаторы EdgeEver перезаписываются.", + confirmImport: "Импорт", + }, + shortcuts: { + global: { + label: "Показать или скрыть окно", + description: "Работает из других приложений. Сочетание хранится только на этом устройстве.", + disabled: "Не задано", + disable: "Отключить", + requireModifiers: "Буква, цифра или F1–F12 и не меньше двух клавиш-модификаторов.", + invalid: "Такое сочетание клавиш не поддерживается.", + unavailable: "Система не смогла зарегистрировать сочетание. Выберите другое или проверьте системные сочетания.", + saveFailed: "Не удалось сохранить сочетание. Повторите.", + }, + title: "Горячие клавиши", + manage: "Управление", + description: "Задайте комбинации клавиш для типичных действий с заметками. Нажмите Esc, чтобы отменить запись.", + recording: "Нажмите сочетание", + reset: "Восстановить по умолчанию", + requireModifier: "Нажмите сочетание с Ctrl, ⌘ или Alt.", + conflict: "Это сочетание уже используется для «{{label}}».", + actions: { + createMemo: { label: "Новая заметка" }, + createNotebook: { label: "Новый блокнот" }, + focusSearch: { label: "Поиск в текущей заметке" }, + focusGlobalSearch: { label: "Поиск по всем заметкам" }, + focusReplace: { label: "Заменить текст" }, + openQuickSwitcher: { label: "Быстрый переход" }, + openPreviousMemo: { label: "Предыдущая заметка" }, + openNextMemo: { label: "Следующая заметка" }, + openAiAssistant: { label: "Открыть ИИ-ассистента" }, + saveAndSync: { label: "Сохранить и синхронизировать" }, + toggleReadingProtection: { label: "Вкл./выкл. защиту чтения" }, + toggleEditorMode: { label: "Переключить режим редактора" }, + toggleOutline: { label: "Показать/скрыть структуру документа" }, + toggleSidebar: { label: "Показать или скрыть боковую панель" }, + }, + }, + quickSwitcher: { + title: "Быстрый переход", + description: "Найдите и откройте любую заметку, кроме находящихся в корзине.", + placeholder: "Поиск по заголовкам или содержимому заметок…", + recent: "Недавно обновлённые", + results: "Результаты поиска", + empty: "Нет подходящих заметок", + loading: "Поиск…", + openHint: "Открыть · Enter", + closeHint: "Закрыть · Esc", + }, + options: { + memoDensity: { + preview: "Список с превью", + compact: "Компактный список", + }, + memoSort: { + updatedDesc: "Недавно обновлённые", + createdDesc: "По дате создания", + titleAsc: "По названию А–Я", + titleDesc: "Название Я–А", + }, + notebookSort: { + custom: "Своя", + nameAsc: "По имени", + memoCountDesc: "По числу заметок", + updatedDesc: "Дата обновления", + }, + memoFilter: { + all: "Все", + pinned: "Закреплённые", + tagged: "С тегами", + untagged: "Без тегов", + }, + }, + mobileSheets: { + selectionCount: "Выбрано: {{count}}", + selectMemo: "Выбрать заметки", + listOptions: "Параметры списка", + displayMode: "Режим отображения", + sortMode: "Сортировка", + tags: "Теги", + assets: "Вложения", + emptyTrash: "Очистить корзину", + trash: "Корзина", + moveToNotebook: "Переместить в блокнот", + searchNotebook: "Поиск блокнотов", + noNotebookFound: "Блокноты не найдены", + currentTarget: "Текущая цель: {{name}}", + moveTo: "Переместить в {{name}}", + bulkActions: "Массовые действия", + mergeMemos: "Объединить заметки", + exportMarkdown: "Экспорт Markdown", + clearSelection: "Снять выделение", + more: "Ещё", + }, + memoList: { + selectionCount: "Выбрано: {{count}}", + selectMemo: "Выбрать заметки", + selectedListNone: "Снять выбор со списка", + selectedListAll: "Выбрать весь список", + noSelectableInList: "В текущем списке нет заметок для выбора", + chooseNotebook: "Выберите блокнот", + move: "Переместить", + mergeMemos: "Объединить заметки", + permanentDelete: "Удалить навсегда", + clearSelection: "Снять выделение", + cancelSearch: "Выйти из поиска", + searchPlaceholder: "Поиск заметок", + clearSearch: "Очистить поиск", + cancel: "Отмена", + switchNotebook: "Сменить блокнот", + bulkActions: "Массовые действия", + listOptions: "Параметры списка", + filterTitle: "Фильтр: {{label}}", + sortTitle: "Сортировка: {{label}}", + previewList: "Список с превью", + compactList: "Компактный список", + emptyTrashTitle: "Очистить корзину. Это действие нельзя отменить", + emptyTrash: "Очистить", + more: "Ещё", + moreActions: "Другие действия со списком", + tags: "Теги", + assets: "Вложения", + trash: "Корзина", + currentNotebook: "Текущий блокнот", + allNotebooks: "Все блокноты", + deletedMemos: "Удалённые заметки", + tagFilter: "Фильтр по тегу", + allMemos: "Все заметки", + memoCount: "{{count}} заметок", + deletedCount: "Удалено: {{count}}", + constrainedCount: "{{label}} · {{count}}", + searchConstraint: "Поиск «{{query}}»", + searchActive: "Поиск", + searchResults: "Результатов: {{count}}", + filterConstraint: "Фильтр: {{label}}", + reset: "Сбросить", + fetchingLatest: "Получение последних заметок", + fetchEmptyTitle: "Заметки ещё не загружены", + fetchEmptyDescription: "Сеть или фоновое восстановление PWA могли кратковременно прервать синхронизацию. Это не считается пустым блокнотом.", + retry: "Повторить", + trashEmptyTitle: "Корзина пуста", + emptyTitle: "Заметок пока нет", + noFilteredTitle: "Нет заметок по фильтру", + trashEmptyDescription: "Удалённые заметки появятся здесь.", + emptyDescription: "Сначала создайте заметку — затем здесь можно просматривать, искать и упорядочивать заметки.", + noFilteredDescription: "Попробуйте другой фильтр или измените поисковый запрос.", + newMemo: "Новая заметка", + dropMarkdownTitle: "Перетащите, чтобы создать заметку", + dropMarkdownDescription: "Поддерживается один файл .md или .markdown", + importMarkdownFailedTitle: "Не удалось импортировать файл Markdown", + importMarkdownSingleFile: "Перетаскивайте по одному файлу Markdown за раз.", + importMarkdownUnsupported: "Поддерживаются только файлы .md и .markdown.", + importMarkdownReadFailed: "Не удалось прочитать файл или создать заметку. Попробуйте ещё раз.", + importScreenshotFailedTitle: "Не удалось сохранить снимок экрана в заметку", + importScreenshotEmpty: "Изображение снимка экрана пустое. Сделайте снимок ещё раз и при первом использовании разрешите EdgeEver запись экрана в системных настройках.", + importScreenshotFailed: "Не удалось сохранить снимок экрана как заметку. Проверьте соединение и попробуйте ещё раз.", + importWeChatFailedTitle: "Не удалось сохранить чат WeChat", + importWeChatUnrecognized: "Этот архив не является экспортом чата WeChat.", + importWeChatFailed: "Не удалось сохранить чат как заметку. Попробуйте ещё раз.", + importWeChatInProgress: "Импорт чата WeChat…", + importWeChatRetryHint: "Экспорт чата всё ещё в папке «Загрузки». Попробуйте импортировать его снова.", + importWeChatRetry: "Повторить импорт", + importSharedFileFailedTitle: "Не удалось сохранить файл", + importSharedFileFailed: "Не удалось сохранить этот файл как заметку. Попробуйте ещё раз.", + importSharedFileUnreadable: "Не удалось прочитать выбранный файл. Отправьте его снова.", + importSharedFileTooLarge: "Этот файл слишком большой, чтобы сохранить его как заметку.", + importSharedFileInProgress: "Сохранение файла как заметки…", + loadingMore: "Загрузка заметок", + openMemo: "Открыть заметку", + unpin: "Открепить", + pinMemo: "Закрепить заметку", + restoreMemo: "Восстановить заметку", + moveToNotebook: "Переместить в блокнот", + deleteMemo: "Удалить заметку", + toggleOffFilter: "Отменить {{label}}", + manualSync: "Синхронизировать вручную", + manualSyncing: "Синхронизация заметок", + manualSyncOffline: "Нельзя синхронизировать без сети", + }, + memoCard: { + yesterday: "Вчера", + createdAt: "Создано {{time}}", + updatedAt: "Обновлено {{time}}", + emptyMemo: "Пустая заметка", + select: "Выбрать {{title}}", + unselect: "Снять выбор {{title}}", + dragMany: "Переместить {{count}} заметок", + dragOne: "Переместить «{{title}}»", + moreActions: "Другие действия", + restoreMemo: "Восстановить заметку", + deleteMemo: "Удалить заметку", + permanentDelete: "Удалить навсегда", + }, + assets: { + title: "Менеджер вложений", + fileCount: "Файлов: {{count}}", + imageCount: "Изображений: {{count}}", + filters: { + all: "Все", + image: "Изображения", + document: "Документы", + other: "Другое", + }, + searchPlaceholder: "Поиск по именам вложений или исходным заметкам...", + gridView: "Сетка", + listView: "Список", + unnamedMemo: "Без названия", + loading: "Загрузка вложений...", + noMatches: "Нет подходящих вложений", + empty: "Вложений пока нет", + noMatchesDescription: "Попробуйте изменить поисковый запрос или фильтр категории", + emptyDescription: "Изображения и вложения, добавленные в заметки, появятся здесь", + previewImage: "Просмотр изображения", + previewPdf: "Просмотр PDF {{filename}}", + pdfThumbnail: "Миниатюра первой страницы PDF: {{filename}}", + downloadOpen: "Скачать / открыть", + deletedMemo: "Удалённая заметка", + fromMemo: "Из: {{source}}", + sourceMemo: "Заметка-источник: {{source}}", + openInNewWindow: "Открыть в новом окне", + deleteTitle: "Удалить вложение", + deleteDescription: "Вложение будет безвозвратно удалено из хранилища. Связанные ссылки в заметке больше не откроются. Это действие нельзя отменить.", + deleteAria: "Удалить вложение {{filename}}", + }, + tagsDialog: { + title: "Теги", + count: "{{count}} тегов", + loading: "Загрузка", + empty: "Тегов пока нет", + nameLabel: "Имя тега", + memoCount: "{{count}} заметок", + renameTitle: "Переименовать тег", + renameAria: "Переименовать тег {{name}}", + deleteTitle: "Удалить тег", + deleteAria: "Удалить тег {{name}}", + deleteConfirmTitle: "Удалить тег #{{name}}", + deleteConfirmDescription: "Этот тег будет удалён из {{count}} заметок. Содержимое заметок не будет удалено.", + deleteConfirmLabel: "Удалить тег", + }, + revisions: { + emptyMemo: "Пустая заметка", + title: "История версий", + loading: "Загрузка", + empty: "Предыдущих версий нет", + timeline: "История", + revisionName: "Редакция {{revision}}", + compareTitle: "Редакция {{revision}} в сравнении с текущим содержимым", + changedLines: "Изменено строк: {{count}}", + noRevisionSelected: "Редакция не выбрана", + restoreVersion: "Восстановить эту версию", + historyVersion: "Историческая версия", + currentContent: "Текущее содержимое", + restoreConfirmTitle: "Восстановить эту историческую версию", + restoreConfirmDescription: "Текущее содержимое будет заменено этой исторической версией, при этом новая редакция всё равно будет создана.", + restoreConfirmLabel: "Восстановить", + formActor: "Отправка формы", + }, + pdfViewer: { + loading: "Загрузка PDF…", + unavailable: "Этот PDF нельзя просмотреть. Вы всё ещё можете скачать его или открыть во внешней программе.", + previewTooLarge: "Просмотр отключён для файлов больше 50 МиБ", + fullscreenLabel: "Просмотр PDF: {{filename}}", + zoomIn: "Увеличить", + zoomOut: "Уменьшить", + fitWidth: "По ширине", + fullscreen: "На весь экран", + exitFullscreen: "Выйти из полноэкранного режима", + download: "Скачать PDF", + openExternal: "Открыть во внешней программе", + collapse: "Свернуть превью", + expand: "Развернуть превью", + previous: "Предыдущий PDF", + next: "Следующий PDF", + }, + wordViewer: { + loading: "Загрузка просмотра Word…", + unavailable: "Этот документ Word нельзя просмотреть. Его всё ещё можно скачать или открыть снаружи.", + previewTooLarge: "Просмотр отключён для файлов больше 10 МиБ", + previewLabel: "Просмотр Word: {{filename}}", + }, + audioPlayer: { + label: "Аудиоплеер: {{filename}}", + unavailable: "Этот аудиоформат нельзя воспроизвести на этом устройстве. Вы всё ещё можете скачать файл или открыть его во внешней программе.", + }, + videoPlayer: { + label: "Видеоплеер: {{filename}}", + unavailable: "Этот видеоформат нельзя воспроизвести на этом устройстве. Вы всё ещё можете скачать файл или открыть его во внешней программе.", + }, + editor: { + imageScale: "Масштаб изображения", + imageAlignment: "Выравнивание изображения", + imageAlignments: { left: "По левому краю", center: "По центру", right: "По правому краю" }, + scaleTo: "Масштаб {{percent}}%", + imageSizeSmall: "Маленький", + imageSizeMedium: "Средний", + imageSizeLarge: "Большой", + imageSizeFull: "На всю ширину", + resizeImage: "Перетащите, чтобы изменить ширину изображения", + previewImage: "Просмотр изображения", + imageGallery: { + toolbar: "Макет галереи изображений", + fitLabel: "Подгонка изображения", + fits: { width: "По ширине", height: "По высоте" }, + layouts: { + auto: "Авто", + "2": "2 колонки", + "3": "3 колонки", + "1": "Одна колонка", + }, + group: "Объединить в галерею", + }, + imageViewer: "Просмотр изображений", + imageZoomIn: "Приблизить", + imageZoomOut: "Отдалить", + closeImagePreview: "Закрыть просмотр", + resourceActions: { + download: "Скачать", + transcribe: "Расшифровать", + saveAs: "Экспорт", + rename: "Переименовать", + delete: "Удалить", + unavailable: "Это действие доступно после синхронизации ресурса и когда заметка доступна для редактирования", + }, + imageActions: { + renameTitle: "Переименовать изображение", + renameDescription: "Новое имя файла будет синхронизировано с ресурсом изображения и текущей заметкой.", + filenameLabel: "Имя файла", + deleteTitle: "Удалить изображение", + deleteDescription: "Изображение будет безвозвратно удалено из хранилища и этой заметки. Это действие нельзя отменить.", + failed: "Не удалось выполнить действие с изображением. Попробуйте снова.", + }, + currentNotebook: "Текущий блокнот", + close: "Закрыть", + searchNotebook: "Поиск блокнотов", + noNotebookFound: "Блокноты не найдены", + currentNotebookAria: "Текущий блокнот: {{name}}", + switchToNotebook: "Перейти к {{name}}", + attachmentLabel: "Вложение: {{filename}}", + attachmentInsertText: "Вложение: {{filename}} {{url}}", + attachmentActions: { + renameTitle: "Переименовать вложение", + renameDescription: "Новое имя файла будет синхронизировано с вложением и текущей заметкой.", + filenameLabel: "Имя файла", + deleteTitle: "Удалить вложение", + deleteDescription: "Вложение будет безвозвратно удалено из хранилища и этой заметки. Это действие нельзя отменить.", + failed: "Не удалось выполнить действие с вложением. Попробуйте снова.", + }, + placeholder: "пробел — ИИ · / команды · @ заметки", + placeholderCommands: "/ команды · @ заметки", + noteBodyAria: "Текст заметки", + markdownSourceAria: "Исходная разметка Markdown", + richTableSourceProtection: "В этой ячейке таблицы есть оформление, которое Markdown не передаёт полностью. Последнее изменение не применено. Правьте структуру в визуальном режиме; текст вне ячейки по-прежнему можно править.", + loading: "Загрузка", + emptySelection: "Выберите или создайте заметку", + saveState: { + saving: "Сохранение", + saved: "Синхронизировано", + queued: "В очереди", + conflict: "Конфликт", + error: "Не удалось сохранить", + unsaved: "Не сохранено", + storageUnavailable: "Локальное хранилище браузера перестало отвечать. Не закрывайте эту страницу, закройте другие вкладки EdgeEver и повторите попытку. Скопируйте локальный черновик, прежде чем очищать данные браузера.", + storageRetry: "Повторить сохранение", + errorReason: { + suspiciousOverwrite: "Сохранение остановлено: заголовок изменился, а почти весь текст заметки исчез. Проверьте заметку, скопируйте нужное и повторите.", + }, + conflictReason: { + revisionWithVersions: "Облачная заметка была обновлена (локальная базовая ревизия {{expected}}, в облаке {{current}}). Частые причины: другая вкладка браузера, другое устройство или изменение в облаке, пока вы были офлайн. Скопируйте локальный черновик, затем используйте облачную версию, прежде чем редактировать снова.", + revision: "Облачная заметка была обновлена в другом месте. Частые причины: другая вкладка браузера, другое устройство или изменение в облаке, пока вы были офлайн. Скопируйте локальный черновик, затем используйте облачную версию, прежде чем редактировать снова.", + content: "Содержимое в облаке изменилось после открытия этой заметки. Скопируйте локальный черновик, затем используйте облачную версию, прежде чем сохранять.", + editSession: "Сеанс редактирования устарел или истёк. Скопируйте локальный черновик, затем используйте облачную версию, прежде чем редактировать снова.", + offlineStale: "Пока вы были офлайн или в очереди, облачная заметка была обновлена, поэтому локальный черновик не может её перезаписать. Скопируйте локальный черновик, затем используйте облачную версию.", + generic: "При сохранении обнаружен конфликт версий. Скопируйте локальный черновик, затем используйте облачную версию, прежде чем редактировать снова.", + }, + conflictAdoptCloud: "Взять облачную и перезагрузить", + conflictAdopting: "Применение облачной…", + conflictAdoptFailed: "Не удалось использовать облачную версию. Проверьте подключение и попробуйте снова.", + conflictCopyDraft: "Скопировать локальный черновик", + conflictCopyDraftDone: "Скопировано", + conflictCopyDraftFailed: "Не удалось скопировать. Попробуйте снова.", + }, + uploadState: { + failed: "Ошибка загрузки", + compressing: "Сжатие", + uploading: "Загрузка", + fileFailed: "Не удалось загрузить файл", + imageCompressing: "Сжатие изображения", + imagePreparing: "Обработка изображения", + waitingToUpload: "Ожидание загрузки", + fileUploading: "Загрузка файла", + }, + notebookFallback: "Блокнот", + saveAndBack: "Сохранить и вернуться к списку", + backToList: "Назад к списку", + previousMemo: "Предыдущая заметка", + nextMemo: "Следующая заметка", + focusMode: "Режим фокуса", + enterFocusMode: "Войти в режим фокуса", + exitFocusMode: "Выйти из режима фокуса", + enableReadingProtection: "Включить защиту чтения", + disableReadingProtection: "Отключить защиту чтения и редактировать", + updatedAt: "Обновлено {{time}}", + done: "Готово", + uploadAttachment: "Загрузить вложение", + collapseFormat: "Свернуть форматирование", + format: "Формат", + searchCurrentMemo: "Поиск в текущей заметке", + replaceCurrentMemo: "Заменить в текущей заметке", + versionHistory: "История версий", + copyNoteId: "Скопировать ID заметки", + copyNoteIdAfterSync: "Скопировать ID заметки после синхронизации", + noteIdCopied: "ID заметки скопирован: {{id}}", + noteIdCopyFailed: "Не удалось скопировать ID заметки.", + exportMarkdown: "Экспорт Markdown", + duplicate: "Дублировать заметку", + duplicateTitle: "{{title}} (копия)", + duplicateFailedTitle: "Не удалось дублировать заметку", + duplicateFailed: "Копия не создана. Повторите чуть позже.", + duplicateOffline: "Чтобы дублировать заметку, нужно соединение с сервером. Повторите, когда будете в сети.", + duplicatePendingChanges: "У заметки есть ещё не синхронизированные изменения. Дождитесь синхронизации и повторите.", + duplicateTooLarge: "В заметке слишком много или слишком большие файлы для дублирования. Можно дублировать до 20 файлов общим размером 64 МиБ.", + duplicateFilesUnavailable: "Часть файлов заметки недоступна, поэтому её нельзя дублировать.", + exportHtml: "Экспорт HTML", + exportPdf: "Экспорт PDF", + exportPng: "Экспорт PNG", + exportJpeg: "Экспорт JPEG", + imageExport: { + error: "Не удалось экспортировать заметку как изображение.", + inProgress: "Экспорт изображения уже выполняется.", + imageEmbedPartial: + "Изображение экспортировано, но {{failed}} из {{total}} изображений заметки не удалось включить.", + imageEmbedFailed: + "Изображение экспортировано, но ни одно из {{total}} изображений заметки не удалось включить.", + }, + imageShare: { + title: "Поделиться как изображением", + description: "Создайте красивую карточку для публикации с настраиваемыми темами, типографикой и макетом.", + previewAlt: "Превью изображения заметки для публикации", + generating: "Создание превью…", + theme: "Тема", + background: "Фон", + previewBody: "Идеи, которыми стоит поделиться", + previewQuote: "Сохраняйте важное.", + themes: { + "gradient": "Классика", + "glass": "Стекло", + "quote": "Цитата", + "book": "Отрывок из книги", + "memo": "Заметка", + "bento": "Bento", + "black-sun": "Чёрное солнце", + "frame": "Рамка", + "handwritten": "Рукописный", + "code": "Код", + }, + backgrounds: { + slate: "Чистый", + mint: "Мята", + warm: "Тёплый", + }, + fontStyle: "Типографика", + templateFonts: { + puhui: "Alibaba PuHuiTi", + misans: "MiSans", + mincho: "Huiwen Mincho", + song: "Source Han Serif", + longcang: "Long Cang", + geist: "Geist Mono", + }, + fontStyles: { + "source-han-serif": "Noto Serif SC", + "anton": "Anton", + "douyin": "Douyin Sans", + "mashan": "Ma Shan Zheng", + "jiangcheng": "Jiangcheng Rounded", + "huangyou": "ZCOOL QingKe HuangYou", + "glow": "Glow Sans", + "kuaile": "ZCOOL KuaiLe", + "source-han-sans": "Noto Sans SC", + "source-han-serif-heavy": "Noto Serif SC Heavy", + "inter": "Inter", + "dmserif": "DM Serif Display", + "bebas": "Bebas Neue", + "spacegrotesk": "Space Grotesk", + "jetbrains": "JetBrains Mono", + + auto: "Как в теме", + kai: "Рукописный кай", + serif: "Редакционный антиква", + sans: "Современный гротеск", + mono: "Geek Mono", + smiley: "Smiley Sans", + wenkai: "LXGW WenKai", + zhuque: "Zhuque Fangsong", + heavy: "Noto Sans SC Heavy", + puhui: "PuHuiTi", + puhuiHeavy: "PuHuiTi Heavy", + youshe: "YouShe Title", + shuhei: "Alimama ShuHei", + }, + remoteFontHint: "Загружаемые шрифты при первом использовании нуждаются в сети и затем кэшируются. PuHuiTi и другие рекомендуемые шрифты — полные исходные файлы большего размера.", + remoteFontError: "Шрифт не загрузился. Подключитесь и повторите или выберите другой шрифт.", + retry: "Повторить", + fontSize: "Размер шрифта", + fontSizes: { + sm: "Компактный", + md: "Стандартная", + lg: "Комфортный", + }, + cardWidth: "Ширина карточки", + cardWidths: { + compact: "Компактная (560px)", + standard: "Стандартная (680px)", + wide: "Широкая (800px)", + }, + metadata: "Элементы содержимого", + fields: { + title: "Название", + notebook: "Блокнот", + tags: "Теги", + updatedAt: "Время обновления", + readingTime: "Время чтения и число слов", + author: "Подпись автора", + branding: "Брендинг EdgeEver", + }, + authorPlaceholder: "Имя или псевдоним", + format: "Формат изображения", + pngHint: "без потерь · чёткий текст", + jpegHint: "сжатый · меньший файл", + previewFitWidth: "По ширине", + previewFitScreen: "Вписать в экран", + previewZoomIn: "Увеличить", + previewZoomOut: "Уменьшить", + share: "Поделиться", + download: "Скачать", + copyImage: "Скопировать изображение", + copied: "Скопировано в буфер обмена", + copyFailed: "Не удалось скопировать — скачайте файл", + action: "Поделиться как изображением", + longImageWarning: "Изображение очень длинное. Некоторые соцсети могут сжать его — для лучшего качества сохраните скачанный оригинал.", + }, + htmlExport: { + error: "Не удалось экспортировать заметку в HTML.", + imageEmbedPartial: + "HTML экспортирован. Встроено {{embedded}} из {{total}} изображений. {{failed}} изображ. не удалось встроить — офлайн они могут не отображаться.", + imageEmbedFailed: + "HTML экспортирован, но ни одно из {{total}} изображений не удалось встроить. Офлайн они могут не отображаться.", + }, + pdfExport: { + preparing: "Подготовка предпросмотра печати…", + ready: "Предпросмотр печати готов", + hint: "В диалоге печати выберите «Сохранить как PDF».", + print: "Печать / Сохранить PDF", + close: "Закрыть", + error: "Не удалось создать предпросмотр печати", + popupBlocked: "Браузер заблокировал окно предпросмотра. Разрешите всплывающие окна для EdgeEver и попробуйте снова.", + }, + save: "Сохранить", + more: "Ещё", + moreAria: "Ещё действия с заметкой", + restoreMemo: "Восстановить заметку", + deleteForever: "Удалить навсегда", + deleteMemo: "Удалить заметку", + notebookPlaceholder: "Текущий блокнот", + tagPlaceholder: "Добавьте теги через запятую", + tagPicker: { + open: "Выберите теги заметки", + title: "Выберите теги", + description: "Выберите существующие теги или введите имя, чтобы создать новый.", + selected: "Выбранные теги", + remove: "Удалить тег {{name}}", + searchPlaceholder: "Найдите или введите новый тег", + create: "Создать", + loading: "Загрузка тегов…", + empty: "Подходящих тегов нет. Можно создать новый.", + memoCount: "{{count}} заметок", + aiGenerateDirect: "Умные теги", + aiGenerating: "Генерация…", + aiAdded: "Добавлено: {{count}}", + aiEmptyShort: "Нет полезных тегов", + aiRetry: "Предложить снова", + aiConfigure: "Сначала настройте модель в «ИИ и интеграции».", + aiFailed: "Не удалось предложить теги.", + }, + searchPlaceholder: "Поиск в текущей заметке", + replacePlaceholder: "Заменить на", + previousSearchResult: "Предыдущий результат", + nextSearchResult: "Следующий результат", + replaceAll: "Заменить все", + closeSearch: "Закрыть поиск", + editMemo: "Редактировать заметку", + openEditor: "Открыть редактор", + paste: "Вставить", + typeInput: "Тип", + clipboardReadFailed: "Не удалось прочитать буфер обмена. Проверьте, что браузер разрешает доступ к нему.", + characterCount: "{{count}} симв.", + copyToWeChat: "Копировать как форматированный текст", + phonePreview: "Превью телефона", + showPhonePreview: "Показать превью телефона", + hidePhonePreview: "Скрыть превью телефона", + phonePreviewEmpty: "Начните писать, чтобы увидеть, как заметка выглядит на телефоне.", + phonePreviewFollow: "Следить за прокруткой", + copyingToWeChat: "Подготовка форматированного текста", + copiedToWeChat: "Скопировано", + copyToWeChatFailed: "Не удалось скопировать", + outline: "Оглавление", + showOutline: "Показать оглавление", + hideOutline: "Скрыть оглавление", + collapseOutlineHeading: "Свернуть {{name}}", + expandOutlineHeading: "Развернуть {{name}}", + }, + notebookSite: { + advancedSettings: "Дополнительные настройки", + noteSites: "Видимость на сайте блокнота", + showInSite: "Показывать на сайте «{{name}}»", + noteSitesHint: "Изменения сразу действуют для выбранного сайта. Другие ссылки доступа настраиваются отдельно.", + manage: "Управление сайтом", + backToNotes: "К заметкам", + content: "Содержание", + chooseHomepage: "Выбрать заметку главной", + useDirectory: "Главная — оглавление", + saveHint: "Сохраните, чтобы применить настройки сайта.", + homepageHint: "Найдите включённые заметки, выберите главную и сохраните настройки.", + homepageFallback: "Выбранная главная недоступна, поэтому посетители видят оглавление. Если вернуть ей видимость, она снова станет главной.", + findNotes: "Искать по названию или тексту", + visibility: "Видимость", + allNotes: "Все заметки", + included: "Публичные", + excludedState: "Исключена", + immediateHint: "Исключение и возврат сразу действуют только для этого сайта.", + page: "Страница {{page}}", + previousPage: "Назад", + nextPage: "Дальше", + noNotes: "Нет подходящих заметок", + selectNote: "Выбрать", + contentSaved: "Видимость обновлена", + noActiveShare: "Для этого блокнота общий доступ не включён или ссылка отозвана.", + fit: "Вписать в окно", + fullscreen: "На весь экран", + exitFullscreen: "Выйти из полноэкранного режима", + + settings: "Настройки сайта", + slug: "Псевдоним URL", + homepage: "Главная", + directory: "Оглавление", + logo: "URL логотипа (HTTPS)", + headerLinks: "Ссылки в шапке", + label: "Подпись", + url: "URL (HTTPS)", + addLink: "Добавить ссылку", + remove: "Удалить", + footer: "Подвал", + indexing: "Разрешить индексацию поисковиками", + indexingHint: "Доступно только без пароля. Публичное содержимое может появиться в результатах поиска.", + sort: "Порядок глав", + existingOrder: "Текущий порядок", + titleOrder: "Название / номер", + save: "Сохранить настройки", + saved: "Настройки сохранены", + chooseNotes: "Найдите заметки, чтобы выбрать главную или исключить их с сайта", + exclude: "Исключить", + restore: "Вернуть", + excluded: "Исключённые заметки", + exclusionHint: "Исключения действуют только для этого сайта. У других ссылок свои правила доступа.", + more: "Показать ещё", + outline: "На этой странице", + collapse: "Свернуть код", + expand: "Развернуть код", + language: "Язык", + search: "Искать на этом сайте", + allTags: "Все метки", + searchHint: "Поиск по названиям и тексту · ⌘/Ctrl K", + light: "Светлая", + dark: "Тёмная", + system: "Как в системе", + theme: "Оформление", + previewUnavailable: "Просмотр недоступен", + homepageSelected: "Выбранная главная", + }, + sharing: { + individual: "Ссылка на отдельную заметку", + createIndividual: "Создать ссылку на заметку", + qrCode: "QR-код", + qrCodeLabel: "QR-код ссылки доступа", + qrCodeHint: "Наведите камеру телефона, чтобы открыть эту заметку.", + qrCodeNotebookHint: "Наведите камеру телефона, чтобы открыть этот блокнот.", + contentLabel: "Содержимое общей заметки", + siteHideContents: "Скрыть оглавление", + siteShowContents: "Показать оглавление", + siteHome: "Главная", + siteSearch: "Искать по названию и тексту", + siteClearSearch: "Очистить поиск", + siteNoteCount: "Заметок: {{count}}", + siteWelcome: "Смотрите заметки и темы и найдите то, что вам интересно.", + siteResults: "Найдено заметок: {{count}}", + siteNoResults: "Нет подходящих заметок. Попробуйте другой запрос.", + sitePoweredBy: "Работает на EdgeEver", + siteSkipToContent: "К содержанию", + siteBreadcrumb: "Навигационная цепочка", + siteReadingNavigation: "Навигация по чтению", + sitePrevious: "Назад", + siteNext: "Дальше", + + notebookChoose: "Выберите блокнот для общего доступа", + notebookTitle: "Поделиться блокнотом", + notebookProtectedScopeHint: "Чтобы смотреть этот блокнот, включая вложенные блокноты и вложения, нужен пароль. Содержимое обновляется само.", + notebookPasswordScopeHint: "Пароль защищает только эту ссылку. Другие общие доступы независимы.", + notebookPasswordRequiredHint: "Этот блокнот защищён паролем. Введите пароль, чтобы открыть оглавление, заметки и вложения.", + notebookScopeHint: "Любой по ссылке может смотреть этот блокнот, включая вложенные блокноты и вложения. Содержимое обновляется само.", + notebookInheritedHint: "Этот блокнот также доступен через родительский. Эти настройки не влияют на родительскую ссылку.", + notebookInheritedOnlyHint: "Уже доступен по родительской ссылке. Можно создать отдельную ссылку со своими настройками.", + notebookCreateIndependent: "Создать отдельную ссылку", + notebookRevokeHint: "Отзыв отключает эту ссылку. Другие доступы останутся.", + notebookPublic: "Публичный блокнот (включая вложенные и будущие заметки)", + notebookContents: "Содержание", + notebookUnavailable: "Этот общий блокнот недоступен", + notebookUnavailableHint: "Ссылку могли отозвать или блокнот удалён.", + notebookNoteUnavailableHint: "Заметка могла выйти из публичного оглавления, быть удалена, или ссылка неверна.", + notebookWelcome: "Выберите заметку в содержании, чтобы начать чтение.", + notebookEmpty: "В этом блокноте пока нечего читать.", + memoManagementTitle: "Доступ к заметкам", + notebookManagementTitle: "Доступ к блокнотам", + notebookManagementEmpty: "Сейчас ни один блокнот не открыт по ссылке.", + + managementTitle: "Управление доступом", + managementDescription: "Заметки и блокноты, доступные по публичным ссылкам, и их настройки доступа.", + managementLoading: "Загрузка общих заметок", + managementLoadFailed: "Не удалось загрузить актуальный список. Проверьте соединение и повторите.", + managementEmpty: "Сейчас ни одна заметка не открыта по ссылке.", + viewAll: "Все общие доступы", + retry: "Повторить", + loadMore: "Показать ещё", + sharedOn: "Открыто {{date}}", + passwordProtected: "Защищено паролем", + unknownNotebook: "Неизвестный блокнот", + action: "Поделиться заметкой", + afterSync: "Поделиться заметкой после синхронизации", + active: "Общий доступ", + manage: "Управление доступом", + notebookInheritedAction: "Доступ через родительский", + title: "Поделиться заметкой", + description: "Любой, у кого есть ссылка, может просмотреть последнюю сохранённую версию этой заметки без входа.", + descriptionProtected: "Любой, у кого есть ссылка, должен ввести пароль, чтобы просмотреть последнюю сохранённую версию этой заметки.", + linkLabel: "Публичная ссылка", + inactiveHint: "Создайте приватную, неугадываемую ссылку, которую можно отозвать в любой момент.", + liveContentHint: "Ссылка всегда показывает последнее сохранённое содержимое. При отзыве доступ к заметке и вложениям отключается сразу.", + create: "Создать ссылку для доступа", + copy: "Копировать ссылку", + copied: "Скопировано", + copyFailed: "Не удалось скопировать", + open: "Открыть общую заметку", + revoke: "Отозвать доступ", + error: "Не удалось обновить доступ. Попробуйте снова.", + passwordToggle: "Пароль доступа", + passwordLabel: "Пароль доступа", + passwordCopy: "Копировать пароль", + copyLinkAndPassword: "Копировать ссылку и пароль", + regeneratePassword: "Сгенерировать заново", + passwordUnavailableHint: "Этот пароль нельзя показать. Создайте новый, чтобы видеть его на всех устройствах; прежний перестанет работать.", + passwordSet: "Пароль задан", + passwordPrefix: "Пароль: ", + passwordRequiredTitle: "Нужен пароль", + passwordRequiredHint: "Эта общая заметка защищена паролем. Введите пароль, чтобы продолжить.", + passwordSubmit: "Открыть заметку", + passwordInvalid: "Неверный пароль. Попробуйте снова.", + passwordRateLimited: "Слишком много попыток. Попробуйте позже.", + readOnly: "Доступ только для чтения", + publicLoading: "Загрузка общей заметки", + publicUpdated: "Обновлено {{time}}", + publicUnavailable: "Общая заметка недоступна", + publicUnavailableHint: "Ссылка может быть недействительной, владелец мог её отозвать, или заметка удалена.", + }, + slashMenu: { + menu: "Меню вставки", + empty: "Подходящих команд нет.", + close: "Закрыть меню", + groups: { + suggested: "Рекомендуемые", + basic: "Базовые блоки", + insert: "Вставка", + }, + items: { + ai: "Спросить ИИ", + currentDate: "Текущая дата", + currentTime: "Текущее время", + currentDateTime: "Дата и время", + }, + }, + editorToolbar: { + toolbar: "Панель редактора", + showMore: "Показать больше инструментов", + showLess: "Свернуть панель редактора", + paragraph: "Текст", + heading1: "Заголовок 1", + heading2: "Заголовок 2", + heading3: "Заголовок 3", + heading4: "Заголовок 4", + heading5: "Заголовок 5", + heading6: "Заголовок 6", + undo: "Отменить", + redo: "Повторить", + bold: "Жирный", + italic: "Курсив", + strike: "Зачёркнутый", + inlineCode: "Код в строке", + bulletList: "Маркированный список", + taskList: "Список задач", + orderedList: "Нумерованный список", + increaseListIndent: "Увеличить уровень списка (Tab)", + decreaseListIndent: "Уменьшить уровень списка (Shift + Tab)", + listIndentHint: "Tab — уровень выше · Shift + Tab — ниже", + quote: "Цитата", + fold: "Свернуть", + codeBlock: "Блок кода", + copyCode: "Копировать код", + codeCopied: "Скопировано", + codeCopyFailed: "Не удалось скопировать", + codeLanguage: "Язык кода", + plainText: "Обычный текст", + mermaidDiagram: "Вставить диаграмму Mermaid", + mermaidPreview: "Превью диаграммы Mermaid", + mermaidRendering: "Отрисовка диаграммы…", + mermaidInvalid: "Неверный синтаксис Mermaid. Отредактируйте исходный код ниже.", + mermaidEmpty: "Введите исходный код Mermaid ниже, чтобы создать диаграмму.", + mermaidSource: "Исходник Mermaid", + mermaidShowSource: "Показать исходник Mermaid", + mermaidHideSource: "Скрыть исходник Mermaid", + mermaidOpenViewer: "Открыть просмотр диаграммы Mermaid", + mermaidViewer: "Просмотр диаграммы Mermaid", + mermaidZoomIn: "Увеличить диаграмму", + mermaidZoomOut: "Уменьшить диаграмму", + mermaidResetZoom: "Сбросить масштаб диаграммы", + mermaidCloseViewer: "Закрыть просмотр диаграммы", + math: "Вставить формулу", + blockMath: "Формула блоком", + horizontalRule: "Разделитель", + themeBlock: "Вставить блок оформления", + deleteThemeBlock: "Удалить оформленный блок", + themeBlocks: { + intro: "Карточка введения", + "key-point": "Карточка ключевой мысли", + callout: "Выноска", + chapter: "Заголовок главы", + }, + table: "Таблица", + insertTable: "Вставить таблицу 3×3", + addTableRow: "Добавить строку ниже", + deleteTableRow: "Удалить текущую строку", + addTableColumn: "Добавить столбец справа", + deleteTableColumn: "Удалить текущий столбец", + toggleTableHeader: "Переключить строку заголовка", + deleteTable: "Удалить таблицу", + markdown: "Перейти к исходной разметке Markdown", + richText: "Перейти к форматированному тексту", + appearance: "Внешний вид и стиль", + blockStyle: "Стиль абзаца", + markdownSource: "Исходная разметка Markdown", + markdownTheme: "Тема исходной разметки Markdown", + editorTheme: "Тема редактора", + switchToMarkdown: "Режим Markdown", + switchToRichText: "Режим форматированного текста", + attachment: "Загрузить вложение", + noteLink: "Ссылка на заметку", + externalLink: "Вставить ссылку", + externalLinkEdit: "Изменить ссылку", + externalLinkShortcut: "Вставить ссылку (Ctrl/⌘K)", + }, + externalLinkDialog: { + title: "Вставить ссылку", + editTitle: "Изменить ссылку", + description: "Добавьте веб-адрес к выделенному тексту или вставьте новую фразу со ссылкой.", + urlLabel: "URL", + urlPlaceholder: "https://example.com", + textLabel: "Отображаемый текст", + textPlaceholder: "Необязательно — по умолчанию URL", + apply: "Применить", + remove: "Удалить ссылку", + errorEmpty: "Введите URL.", + errorInvalid: "Введите корректный URL, например example.com или https://example.com.", + errorUnsupported: "Эта схема URL не допускается.", + }, + mathFormulaDialog: { + title: "Вставить формулу", + editTitle: "Изменить формулу", + description: "Напишите формулу на LaTeX. Строчная вставляется в предложение; выносная занимает отдельную строку.", + inline: "Строчная", + block: "Выносная", + latexLabel: "LaTeX", + latexPlaceholder: "E = mc^2", + preview: "Просмотр", + previewEmpty: "Предпросмотр появится после ввода LaTeX", + previewError: "Не удалось отобразить эту формулу", + apply: "Применить", + remove: "Удалить формулу", + errorEmpty: "Введите LaTeX.", + visualLabel: "Формула", + sourceMode: "Исходник LaTeX", + visualMode: "Визуальный редактор", + loading: "Загрузка редактора формул…", + loadError: "Визуальный редактор не загрузился. Можно продолжить в LaTeX.", + incomplete: "Введите текст в пустой слот; Tab — следующий", + }, + noteLinkPicker: { + title: "Ссылка на заметку", + searchPlaceholder: "Поиск заметок для ссылки", + empty: "Заметки не найдены", + loading: "Поиск заметок...", + close: "Закрыть выбор заметки", + openHint: "Удерживайте {{modifier}} и нажмите, чтобы открыть ссылку. Чтобы открывать ссылки одним кликом, отключите эту опцию в Профиль → Основные", + }, + mobileNotebookPicker: { + title: "Сменить блокнот", + current: "Текущий: {{name}}", + allMemos: "Все заметки", + notebookFallback: "Блокнот", + searchPlaceholder: "Поиск блокнотов", + clearSearch: "Очистить поиск", + currentAll: "Сейчас: все заметки", + switchAll: "Перейти ко всем заметкам", + matchedNotebooks: "Подходящие блокноты", + notebooks: "Блокноты", + collapseAllAria: "Свернуть все блокноты", + expandAllAria: "Развернуть все блокноты", + collapseAll: "Свернуть все", + expandAll: "Развернуть все", + noSearchResult: "Нет результатов по запросу «{{query}}»", + noNotebook: "Блокноты не найдены", + showAll: "Показать все блокноты", + collapse: "Свернуть {{name}}", + expand: "Развернуть {{name}}", + currentNotebook: "Текущий: {{name}}", + switchToNotebook: "Перейти к {{name}}", + }, + workspaceDialogs: { + openMemoFailed: "Не удалось открыть заметку. Она может быть недоступна или в корзине. Проверьте соединение и повторите.", + emptyTrashTitle: "Очистить корзину", + emptyTrashDescription: "Все заметки в корзине и связанные вложения будут удалены. Это нельзя отменить.", + emptyTrashConfirm: "Очистить корзину", + emptyTrashFailedTitle: "Не удалось очистить корзину", + emptyTrashFailedDescription: "Заметки восстановлены в корзину. Попробуйте снова.", + deleteNotebookTitle: "Удалить блокнот «{{name}}»", + deleteNotebookDescription: "Пустые вложенные блокноты удалятся вместе с ним. Сначала переместите или удалите заметки внутри. Восстановить отсюда нельзя.", + deleteNotebookFailedTitle: "Не удалось удалить этот блокнот", + deleteNotebookNotEmpty: "В этом блокноте или во вложенных ещё есть заметки. Переместите или удалите их и попробуйте снова.", + deleteNotebookFailed: "Блокнот всё ещё здесь. Попробуйте снова.", + ok: "ОК", + resizeNotebookSidebar: "Изменить ширину панели блокнотов", + resizeMemoList: "Изменить ширину списка заметок", + resizeMemoListHint: "Перетащите, чтобы изменить ширину колонки списка; двойной щелчок сбрасывает; стрелки — для точной настройки", + }, + evernoteGuide: { + title: "Руководство по миграции из Evernote", + subtitle: "Миграция с помощью AI Agent с сохранением иерархии стеков блокнотов", + }, + evernoteImport: { + title: "Импорт из Evernote, flomo, Notion и Memos", + guide: "Открыть руководство по миграции", + openGuideAria: "Открыть руководство по миграции из Evernote в новой вкладке", + description: "Перенесите заметки со сторонних платформ с рекомендуемым пакетным импортом через MCP и AI Agent.", + }, + session: { + loggingOut: "Выход...", + logout: "Выйти", + }, + localDataReset: { + title: "Опасная зона", + description: "Удалить все данные EdgeEver с этого Mac и вернуть приложение к состоянию первого запуска.", + action: "Очистить локальные данные", + dialogTitle: "Удалить данные EdgeEver с этого Mac?", + dialogDescription: "Будут удалены все локальные заметки, несинхронизированные изменения, офлайн-вложения, кэши, локальные резервные копии, настройки приложения и данные входа.", + remoteUnaffected: "Данные на сервере и экспортированные файлы не будут затронуты. После очистки приложение перезапустится автоматически.", + warning: "Это действие нельзя отменить. Все незасинхронизированные заметки и изменения будут безвозвратно утеряны.", + confirm: "Очистить локальные данные", + clearing: "Очистка...", + errors: { + unsafeDataDirectory: "EdgeEver не удалось проверить, что локальную папку с данными можно безопасно удалить. Ваши данные не изменились.", + applicationBundleNotFound: "EdgeEver не удалось определить установленное приложение, необходимое для перезапуска после очистки. Переустановите EdgeEver в Applications и попробуйте снова.", + helperStartFailed: "EdgeEver не удалось запустить вспомогательную программу очистки данных macOS. Ваши данные не изменились. Перезапустите приложение и попробуйте снова.", + unexpected: "Произошла непредвиденная ошибка до очистки локальных данных. Ваши данные не изменились. Перезапустите приложение и попробуйте снова.", + }, + }, + loginDevices: { + title: "Устройства со входом", + loading: "Загрузка устройств со входом...", + loadFailed: "Не удалось загрузить устройства со входом. Попробуйте снова.", + empty: "Нет активных устройств со входом.", + current: "Текущее устройство", + mobile: "Телефон", + tablet: "Планшет", + desktop: "Компьютер", + unknownDevice: "Неизвестное устройство", + unknownBrowser: "Неизвестный браузер", + labelPlaceholder: "Например: Рабочий компьютер", + editLabel: "Изменить название устройства", + saveLabel: "Сохранить название устройства", + cancelLabel: "Отменить изменение названия устройства", + location: "Местоположение: {{location}}", + lastSeen: "Последняя активность: {{time}}", + signedIn: "Вход выполнен: {{time}}", + revokeDevice: "Выйти с устройства", + revokeOthers: "Выйти со всех остальных устройств", + revokeDeviceTitle: "Выйти с этого устройства?", + revokeDeviceDescription: "Этот сеанс будет немедленно отозван, и на устройстве потребуется войти снова.", + revokeOthersTitle: "Выйти со всех остальных устройств?", + revokeOthersDescription: "Все сеансы этой учётной записи, кроме текущего устройства, будут немедленно отозваны.", + revokeConfirm: "Выйти", + revoking: "Выход...", + revokeFailed: "Не удалось выйти с устройства. Попробуйте снова.", + }, + users: { + columns: { + account: "Аккаунт", + role: "Роль", + storage: "Занято", + files: "Файлы", + status: "Статус", + created: "Создан", + login: "Последний вход", + actions: "Действия", + }, + toggleAccount: "Переключить статус аккаунта {{username}}", + accountStorageUsage: "Вложения: {{size}} · файлов: {{count}}", + storageUsage: "Место вложений всех пользователей", + storageUsageFailed: "Не удалось загрузить занятое место вложений.", + + title: "Участники", + description: "Создавайте изолированные личные пространства заметок для семьи или участников команды. Публичная регистрация остаётся отключённой.", + loading: "Загрузка учётных записей...", + createdAt: "Создан {{time}}", + lastLoginAt: "Последний вход {{time}}", + noLoginRecord: "Входов не было", + create: "Добавить участника", + creating: "Создание...", + createTitle: "Добавить нового участника", + createDescription: "Учётная запись получает изолированные заметки, вложения, Корзину и токены MCP.", + username: "Имя пользователя", + usernamePlaceholder: "Например: alex", + usernameExists: "Такое имя пользователя уже существует. Выберите другое.", + displayName: "Отображаемое имя", + displayNamePlaceholder: "Необязательно, например: Alex", + password: "Начальный пароль", + passwordPlaceholder: "Введите не менее 8 символов", + passwordHint: "Участник сможет сменить этот пароль в Профиле после входа.", + newPassword: "Новый пароль (от 8 символов)", + resetPassword: "Сбросить пароль", + resetTitle: "Сбросить пароль для {{username}}", + resetDescription: "После сброса остальные сеансы этой учётной записи будут отозваны.", + enabled: "Включено", + disabled: "Отключено", + failed: "Операция с учётной записью не удалась. Проверьте введённые данные и попробуйте снова.", + roles: { owner: "Владелец экземпляра", member: "Участник" }, + }, + password: { + title: "Сменить пароль", + currentPassword: "Текущий пароль", + newPassword: "Новый пароль", + confirmPassword: "Подтвердите новый пароль", + change: "Сменить пароль", + changing: "Смена...", + changed: "Пароль успешно изменён.", + currentPasswordIncorrect: "Текущий пароль неверен.", + passwordMismatch: "Новые пароли не совпадают.", + newPasswordTooShort: "Новый пароль должен содержать не менее 8 символов.", + changeFailed: "Не удалось сменить пароль. Попробуйте снова.", + demoReadOnly: "В демо-среде используются фиксированные учётные данные, смена пароля недоступна.", + }, + mcp: { + copyToken: "Копировать токен", + copyConfig: "Копировать конфигурацию MCP", + bearerPlaceholder: "Bearer мой-токен", + title: "API-токены и конфигурация MCP", + example: "Пример конфигурации", + exampleTitle: "Пример удалённой конфигурации MCP", + copyExample: "Копировать пример", + accessLevelTitle: "Уровень доступа", + accessLevels: { + full: { + label: "Полный доступ", + description: "Чтение, создание и редактирование всех заметок, блокнотов, тегов и вложений, а также перемещение заметок в корзину.", + }, + "read-only": { + label: "Только чтение", + description: "Чтение всех заметок, блокнотов, тегов и вложений без внесения изменений.", + }, + "legacy-custom": { + label: "Устаревший настраиваемый доступ", + }, + }, + loadingTokens: "Загрузка токенов...", + emptyTokens: "Нет API-токенов", + noScope: "Нет областей доступа", + lastUsedAt: "Последнее использование: {{time}}", + neverUsed: "Никогда не использовался", + legacyTokenHint: "Открытый текст старых токенов восстановить нельзя. Создайте новый.", + legacyTokenCannotCopy: "Старый токен нельзя скопировать", + legacyConfigCannotCopy: "Конфигурацию MCP для старых токенов скопировать нельзя", + deleteToken: "Удалить токен", + description: "Разрешите Web Clipper, сервисам MCP и AI-агентам доступ к вашим заметкам.", + namePlaceholder: "Имя токена (например, Chrome Clipper или Claude Desktop)", + createToken: "Создать токен", + activeTokens: "Список токенов", + deleteConfirmTitle: "Удалить токен «{{name}}»?", + deleteConfirmDescription: "Это действие нельзя отменить. После удаления все клиенты, использующие этот токен для API или MCP, сразу потеряют доступ.", + deleteConfirmLabel: "Удалить токен", + scopes: { + "read:notebooks": "Читать блокноты", + "write:notebooks": "Управлять блокнотами", + "read:memos": "Читать заметки", + "write:memos": "Управлять заметками", + "read:resources": "Читать вложения", + "write:resources": "Управлять вложениями", + "read:tags": "Читать теги", + "write:tags": "Управлять тегами", + "ai:generate": "Создавать сводки видеозаметок", + }, + }, + advancedPlay: { + title: "Расширенные сценарии", + description: "Продвинутые сценарии с ИИ-агентами.", + copyPrompt: "Копировать промпт", + prompts: { + persona: { + title: "Профиль персоны", + prompt: "Используй EdgeEver MCP, чтобы прочитать мои заметки и составить профиль персоны на основе реального содержимого. Делай выводы только по фактам из заметок, не ставь психологических диагнозов и не преувеличивай черты. Включи долгосрочные темы, рабочие предпочтения, сигналы компетенций, повторяющиеся проблемы, недавнее направление и после каждого вывода укажи связанные названия заметок или memo id.", + }, + knowledgeMap: { + title: "Карта знаний", + prompt: "Используй EdgeEver MCP, чтобы прочитать мои заметки и построить карту знаний. Определи основные области знаний, ключевые понятия в каждой, связанные заметки, что я уже понимаю и какие пробелы ещё нужно закрыть. Структурируй результат так, чтобы им было удобно пользоваться для дальнейшего обучения и письма.", + }, + tagAdvice: { + title: "Предложения тегов", + prompt: "Используй EdgeEver MCP, чтобы прочитать мои заметки и существующие теги, затем предложи более понятную систему тегов. Укажи дубликаты, слишком узкие, слишком широкие или непоследовательно названные теги и предложи объединения, переименования и новые теги. Пока не изменяй заметки. Дождись моего подтверждения, прежде чем применять изменения.", + }, + }, + }, + templates: { + title: "Центр шаблонов", + create: "Новый шаблон", + useTemplate: "Новая заметка из шаблона", + useThisTemplate: "Использовать шаблон", + previewTemplate: "Просмотр", + previewTitle: "Предпросмотр шаблона", + createDescription: "Создайте переиспользуемый шаблон с нуля.", + myTemplates: "Шаблоны", + savedDescription: "Переиспользуемый шаблон заметки.", + emptyMyTemplates: "Пока нет шаблонов", + emptyMyTemplatesHint: "Нажмите «Новый шаблон» выше или сохраните любую заметку как шаблон прямо в редакторе.", + delete: "Удалить шаблон", + copyId: "Копировать ID шаблона", + idCopied: "ID шаблона скопирован: {{id}}", + idCopyFailed: "Не удалось скопировать ID шаблона.", + deleteConfirmTitle: "Удалить шаблон «{{name}}»", + deleteConfirmDescription: "Вы уверены, что хотите удалить этот шаблон? Это действие нельзя отменить.", + saveAsTemplate: "Сохранить как шаблон", + saveAsTemplateSuccess: "Сохранено как шаблон", + edit: "Редактировать шаблон", + editTemplateTitle: "Редактировать шаблон", + createTemplateTitle: "Создать шаблон", + name: "Название шаблона", + namePlaceholder: "напр., Еженедельное резюме / Карточка чтения", + descriptionField: "Описание шаблона", + descriptionPlaceholder: "Кратко опишите, когда и как использовать этот шаблон", + noteTitle: "Заголовок заметки по умолчанию", + noteTitlePlaceholder: "напр., [Еженедельно] Без названия (оставьте пустым, чтобы использовать название шаблона)", + tags: "Предустановленные теги", + tagsPlaceholder: "напр., работа, резюме (через запятую)", + content: "Markdown-содержимое шаблона", + contentPlaceholder: "Введите предустановленное Markdown-содержимое...", + rawEditor: "Исходная разметка Markdown", + previewEditor: "Просмотр", + templateNamePrompt: "Введите название шаблона", + templateSaved: "Шаблон сохранён", + templateCreated: "Шаблон создан", + unavailable: "Выберите доступный блокнот перед созданием заметки.", + items: memoTemplateSeedTranslations("ru"), + }, + pwa: { + checkingTitle: "Применение обновления", + checkingDescription: "Применяется обновление. Приложение скоро обновится.", + readyTitle: "Новая версия готова", + readyDescription: "Обновите страницу, чтобы получить новые возможности.", + refreshNow: "Обновить сейчас", + closeNotice: "Закрыть уведомление об обновлении", + sidebarDownloads: "Скачать клиенты", + sidebarDownloadsTitle: "Скачать клиенты EdgeEver и расширения браузера", + sidebarGroupApps: "Приложения и клиенты", + sidebarGroupClippers: "Веб-клиппер", + sidebarMac: "macOS", + sidebarWindows: "Windows", + sidebarWindowsAvailability: "Неподписанный установщик — Windows или ваша организация могут предупредить или заблокировать его. Скачивайте только с официального GitHub Release.", + sidebarLinux: "Linux", + sidebarLinuxAvailability: "x86_64 AppImage — поддержка дистрибутива по возможности. Скачивайте только с официального GitHub Release.", + sidebarOtherDownloads: "Другие загрузки клиентов", + sidebarAndroid: "Android", + sidebarAndroidGooglePlay: "Google Play", + sidebarAndroidApk: "Прямой APK", + sidebarAndroidTitle: "Скачать EdgeEver для Android из Google Play", + sidebarIos: "iOS", + sidebarIosBadge: "App Store", + sidebarIosRegionBadge: "Не mainland", + sidebarIosTitle: "Скачать EdgeEver для iOS из App Store (нужен Apple ID не из mainland China)", + sidebarIosAvailability: "Требуется Apple ID не из mainland China", + sidebarChromeEdge: "Chrome / Edge", + sidebarFirefox: "Firefox", + sidebarWebStoreBadge: "Web Store", + sidebarAddonsBadge: "Add-ons", + sidebarClipper: "Веб-клиппер", + sidebarClipperTitle: "Установить EdgeEver Web Clipper", + iosPrompt: { + title: "Добавить EdgeEver на домашний экран", + subtitle: "Полноэкранное отдельное окно — работайте так же плавно, как в нативном приложении.", + step1: "Нажмите кнопку «Поделиться» внизу Safari", + step2: "Найдите и выберите «На экран „Домой“» в меню", + dismiss: "Понятно", + }, + }, + demo: { + resetTitle: "Сбросить демо-окружение", + resetDescription: "Восстанавливает примеры заметок и блокнотов в демо-окружении до исходного состояния. Все правки и изменения будут сброшены.", + resetConfirm: "Подтвердить сброс", + resetButton: "Сбросить демо-окружение", + resetTooltip: "Сбросить примеры заметок и блокнотов в демо-окружении", + resetSuccess: "Демо-окружение успешно сброшено!", + resetFailed: "Не удалось сбросить демо-окружение. Попробуйте позже.", + resetting: "Сброс...", + privacyNotice: "Демо-окружение. Пожалуйста, не сохраняйте личные заметки.", + }, +} as const; diff --git a/packages/shared/src/memo-template-seeds.ts b/packages/shared/src/memo-template-seeds.ts index 396b8163..739493aa 100644 --- a/packages/shared/src/memo-template-seeds.ts +++ b/packages/shared/src/memo-template-seeds.ts @@ -1,6 +1,6 @@ import { resolveSupportedLocale } from "./i18n/locales"; -export type MemoTemplateSeedLocale = "zh-CN" | "en-US" | "ja" | "pl"; +export type MemoTemplateSeedLocale = "zh-CN" | "en-US" | "ja" | "pl" | "ru"; export type MemoTemplateSeedTranslation = { title: string; @@ -21,9 +21,10 @@ const seed = ( enUS: MemoTemplateSeedTranslation, ja: MemoTemplateSeedTranslation, pl: MemoTemplateSeedTranslation, + ru: MemoTemplateSeedTranslation, ): MemoTemplateSeed => ({ ...metadata, - translations: { "zh-CN": zhCN, "en-US": enUS, ja, pl }, + translations: { "zh-CN": zhCN, "en-US": enUS, ja, pl, ru }, }); export const DEFAULT_MEMO_TEMPLATE_SEEDS: readonly MemoTemplateSeed[] = [ @@ -49,6 +50,11 @@ export const DEFAULT_MEMO_TEMPLATE_SEEDS: readonly MemoTemplateSeed[] = [ description: "Zapisuj ulotne myśli, pomysły, linki i działania do natychmiastowego podjęcia.", contentMarkdown: "## 💡 Ulotne myśli\n\n- \n\n## 📌 Kontekst i uwagi\n\n\n\n## 🚀 Kolejne kroki\n\n- [ ] ", }, + { + title: "Быстрая заметка", + description: "Записывайте мимолётные мысли, идеи, ссылки и ближайшие действия.", + contentMarkdown: "## 💡 Мимолётные мысли\n\n- \n\n## 📌 Контекст и заметки\n\n\n\n## 🚀 Дальше\n\n- [ ] ", + }, ), seed( { key: "meeting", translationKey: "meeting", tag: "meeting" }, @@ -72,6 +78,11 @@ export const DEFAULT_MEMO_TEMPLATE_SEEDS: readonly MemoTemplateSeed[] = [ description: "Uporządkowany zapis agendy, kluczowych decyzji i zadań z osobami odpowiedzialnymi.", contentMarkdown: "# 📝 Notatka ze spotkania\n\n- **Termin**:\n- **Prowadzący/protokolant**:\n- **Uczestnicy**:\n\n---\n\n## 🎯 Cel\n\n- \n\n## 💬 Dyskusja i decyzje\n\n1. **[Temat 1]**\n - Najważniejsze punkty:\n - ✅ **Decyzja**:\n\n2. **[Temat 2]**\n - Najważniejsze punkty:\n - ✅ **Decyzja**:\n\n## 📋 Zadania do wykonania\n\n- [ ] **[Osoba odpowiedzialna]** Opis zadania (termin: MM-DD)\n- [ ] **[Osoba odpowiedzialna]** Opis zadania (termin: MM-DD)\n", }, + { + title: "Протокол встречи", + description: "Структурированная запись повестки, решений и задач с ответственными.", + contentMarkdown: "# 📝 Протокол встречи\n\n- **Время**:\n- **Ведущий/запись**:\n- **Участники**:\n\n---\n\n## 🎯 Цель\n\n- \n\n## 💬 Обсуждение и решения\n\n1. **[Тема 1]**\n - Главное:\n - ✅ **Решение**:\n\n2. **[Тема 2]**\n - Главное:\n - ✅ **Решение**:\n\n## 📋 Задачи\n\n- [ ] **[Ответственный]** Описание задачи (срок: ММ-ДД)\n- [ ] **[Ответственный]** Описание задачи (срок: ММ-ДД)\n", + }, ), seed( { key: "weekly-review", translationKey: "weeklyReview", tag: "weekly-review" }, @@ -95,6 +106,11 @@ export const DEFAULT_MEMO_TEMPLATE_SEEDS: readonly MemoTemplateSeed[] = [ description: "Podsumuj najważniejsze osiągnięcia tygodnia, blokery i kluczowe priorytety na kolejny tydzień.", contentMarkdown: "# 🗓️ Tygodniowy raport postępów\n\n## 🌟 Najważniejsze osiągnięcia\n\n- [x] **[Projekt/funkcja]** Szczegóły osiągnięcia\n- [x] **[Projekt/funkcja]** Szczegóły osiągnięcia\n\n## 🚧 Blokery i ryzyka\n\n- ⚠️ **Bloker**: Przyczyna i potrzebne wsparcie\n\n## 🎯 Priorytety na kolejny tydzień\n\n- [ ] \n- [ ] \n- [ ] \n\n## 💡 Refleksje i wnioski\n\n- \n", }, + { + title: "Итоги недели", + description: "Главное за неделю, препятствия и приоритеты на следующую.", + contentMarkdown: "# 🗓️ Отчёт за неделю\n\n## 🌟 Главное\n\n- [x] **[Проект/функция]** Что сделано\n- [x] **[Проект/функция]** Что сделано\n\n## 🚧 Препятствия и риски\n\n- ⚠️ **Препятствие**: причина и какая помощь нужна\n\n## 🎯 Приоритеты на следующую неделю\n\n- [ ] \n- [ ] \n- [ ] \n\n## 💡 Выводы\n\n- \n", + }, ), seed( { key: "reading", translationKey: "reading", tag: "reading" }, @@ -118,6 +134,11 @@ export const DEFAULT_MEMO_TEMPLATE_SEEDS: readonly MemoTemplateSeed[] = [ description: "Wyodrębnij kluczowe wnioski, cytaty, własne refleksje i powiązane zagadnienia.", contentMarkdown: "# 📖 Karta lektury\n\n- **Książka/artykuł**:\n- **Autor/źródło**:\n- **Ocena**: ⭐⭐⭐⭐⭐\n\n---\n\n## 💡 Najważniejszy wniosek\n\n> \n\n## ✍️ Najciekawsze fragmenty i cytaty\n\n> [Treść cytatu]\n> —— *Źródło*\n\n## 🧠 Własne refleksje\n\n- \n\n## 🔗 Działanie i praktyka\n\n- [ ] **Plan działania**:\n", }, + { + title: "Карточка чтения", + description: "Главные выводы, цитаты, собственные мысли и связанные идеи.", + contentMarkdown: "# 📖 Карточка чтения\n\n- **Книга/статья**:\n- **Автор/источник**:\n- **Оценка**: ⭐⭐⭐⭐⭐\n\n---\n\n## 💡 Главный вывод\n\n> \n\n## ✍️ Цитаты\n\n> [Текст цитаты]\n> —— *Источник*\n\n## 🧠 Свои мысли\n\n- \n\n## 🔗 Что сделать\n\n- [ ] **План**:\n", + }, ), seed( { key: "okr", translationKey: "okr", tag: "okr" }, @@ -141,6 +162,11 @@ export const DEFAULT_MEMO_TEMPLATE_SEEDS: readonly MemoTemplateSeed[] = [ description: "Określ cele OKR, kluczowe rezultaty, kamienie milowe i listy zadań.", contentMarkdown: "# 🎯 Podział celu\n\n- **Okres**:\n- **Osoba odpowiedzialna**:\n\n---\n\n## 📌 Cel (Objective)\n\n> \n\n## 📈 Kluczowe rezultaty (Key Results)\n\n- **KR 1**: Docelowy wskaźnik -> Obecny postęp\n- **KR 2**: Docelowy wskaźnik -> Obecny postęp\n\n## 🗓️ Kamienie milowe\n\n- [ ] **Etap 1 (data)**: Cel etapu\n- [ ] **Etap 2 (data)**: Cel etapu\n\n## 📋 Lista zadań do wykonania\n\n- [ ] \n- [ ] \n", }, + { + title: "Цели и разбиение задач", + description: "Цели OKR, ключевые результаты, вехи и список дел.", + contentMarkdown: "# 🎯 Разбор цели\n\n- **Период**:\n- **Ответственный**:\n\n---\n\n## 📌 Цель (Objective)\n\n> \n\n## 📈 Ключевые результаты (Key Results)\n\n- **KR 1**: целевой показатель -> текущий прогресс\n- **KR 2**: целевой показатель -> текущий прогресс\n\n## 🗓️ Вехи\n\n- [ ] **Этап 1 (дата)**: результат этапа\n- [ ] **Этап 2 (дата)**: результат этапа\n\n## 📋 Список дел\n\n- [ ] \n- [ ] \n", + }, ), ]; diff --git a/packages/shared/src/mobile-content-compatibility.ts b/packages/shared/src/mobile-content-compatibility.ts index 0dbf6ace..01abcf51 100644 --- a/packages/shared/src/mobile-content-compatibility.ts +++ b/packages/shared/src/mobile-content-compatibility.ts @@ -71,16 +71,17 @@ const isTextNode = (node: TiptapNode | TiptapTextNode): node is TiptapTextNode = const getOriginalType = (attrs: Record | undefined) => typeof attrs?.originalType === "string" ? attrs.originalType : "unknown"; -const getFallbackLabel = (type: string, locale?: "zh-CN" | "en-US" | "ja" | "pl") => { +const getFallbackLabel = (type: string, locale?: "zh-CN" | "en-US" | "ja" | "pl" | "ru") => { if (locale === "zh-CN") return `暂不支持的内容:${type}`; if (locale === "ja") return `未対応のコンテンツ: ${type}`; if (locale === "pl") return `Nieobsługiwana zawartość: ${type}`; + if (locale === "ru") return `Неподдерживаемое содержимое: ${type}`; return `Unsupported content: ${type}`; }; const fallbackAttrs = ( node: TiptapNode | TiptapTextNode, - locale?: "zh-CN" | "en-US" | "ja" | "pl", + locale?: "zh-CN" | "en-US" | "ja" | "pl" | "ru", ) => ({ originalType: node.type, originalJson: JSON.stringify(node), @@ -228,7 +229,7 @@ const prepareTextNode = (node: TiptapTextNode): TiptapTextNode => { */ export const prepareNativeEditorContent = ( doc: TiptapDoc, - locale?: "zh-CN" | "en-US" | "ja" | "pl", + locale?: "zh-CN" | "en-US" | "ja" | "pl" | "ru", ): TiptapDoc => { const visit = ( node: TiptapNode | TiptapTextNode, diff --git a/packages/shared/src/mobile-editor.ts b/packages/shared/src/mobile-editor.ts index 212f152e..bd40984a 100644 --- a/packages/shared/src/mobile-editor.ts +++ b/packages/shared/src/mobile-editor.ts @@ -2,7 +2,7 @@ import type { Editor } from "@tiptap/core"; import { EditorState } from "@tiptap/pm/state"; import type { ImageWidthPresetId } from "./image-display"; -export type MobileEditorLocale = "zh-CN" | "en-US" | "ja" | "pl"; +export type MobileEditorLocale = "zh-CN" | "en-US" | "ja" | "pl" | "ru"; export type MobileEditorToolbarActionId = | "undo" @@ -132,6 +132,29 @@ const MOBILE_EDITOR_COPY = { full: "Pełna szerokość", }, }, + ru: { + placeholder: "Начните запись...", + toolbar: "Панель редактора", + actions: { + undo: "Отменить", + redo: "Повторить", + image: "Загрузить изображение", + bold: "Полужирный", + bulletList: "Маркированный список", + taskList: "Список задач", + increaseListIndent: "Увеличить уровень списка (Tab)", + decreaseListIndent: "Уменьшить уровень списка (Shift + Tab)", + blockquote: "Цитата", + horizontalRule: "Горизонтальная линия", + }, + imageScale: "Размер изображения", + imageSizes: { + small: "Маленький", + medium: "Средний", + large: "Большой", + full: "На всю ширину", + }, + }, } as const; export const getMobileEditorPlaceholder = (locale: MobileEditorLocale): string => diff --git a/packages/shared/src/note-image-card.test.mjs b/packages/shared/src/note-image-card.test.mjs index 378a6ad6..443a3a85 100644 --- a/packages/shared/src/note-image-card.test.mjs +++ b/packages/shared/src/note-image-card.test.mjs @@ -9,6 +9,7 @@ import { zhCN } from "./i18n/zh-CN"; import { enUS } from "./i18n/en-US"; import { ja } from "./i18n/ja"; import { pl } from "./i18n/pl"; +import { ru } from "./i18n/ru"; import { NOTE_IMAGE_FONT_SUBSETS } from "./note-image-font-manifest"; describe("note image templates", () => { @@ -44,7 +45,7 @@ describe("note image templates", () => { for (const theme of NOTE_IMAGE_THEME_OPTIONS) { expect(NOTE_IMAGE_THEMES[theme]).toBeDefined(); expect(NOTE_IMAGE_BACKGROUND_COLORS[theme]).toMatch(/^#[\da-f]{6}$/i); - for (const locale of [zhCN, enUS, ja, pl]) expect(locale.editor.imageShare.themes[theme]).toBeTruthy(); + for (const locale of [zhCN, enUS, ja, pl, ru]) expect(locale.editor.imageShare.themes[theme]).toBeTruthy(); } for (const retired of ["paper", "press", "highlight", "slate", "mint", "aurora", "sunset", "terminal"]) { expect(NOTE_IMAGE_THEME_OPTIONS).not.toContain(retired); diff --git a/scripts/verify-web-performance.mjs b/scripts/verify-web-performance.mjs index 4b6ad2ad..90b3e27a 100644 --- a/scripts/verify-web-performance.mjs +++ b/scripts/verify-web-performance.mjs @@ -43,7 +43,7 @@ assert.ok(initialModulePreloadBytes <= INITIAL_MODULE_PRELOAD_BUDGET, `Initial m const startupGraph = readWebStartupGraph(distDirectory); assert.ok(startupGraph.initialJavaScriptBytes <= 1024 * 1024, `Eager JavaScript exceeds 1 MiB: ${startupGraph.initialJavaScriptBytes}`); -const eagerOptionalPattern = /(?:^|\/)(?:vendor-(?:code-highlight|beautiful-mermaid|mermaid|tiptap|prosemirror|codemirror|x6|infographic|streamdown)|i18n-(?:ja|pl)-|(?:WorkspaceApp|EditorPane|DiagramEditorPane|PosterEditorPane|TableEditorPane|InfographicEditorPane)-)/; +const eagerOptionalPattern = /(?:^|\/)(?:vendor-(?:code-highlight|beautiful-mermaid|mermaid|tiptap|prosemirror|codemirror|x6|infographic|streamdown)|i18n-(?:ja|pl|ru)-|(?:WorkspaceApp|EditorPane|DiagramEditorPane|PosterEditorPane|TableEditorPane|InfographicEditorPane)-)/; assert.ok(!startupGraph.initialJavaScriptFiles.some(file => eagerOptionalPattern.test(file)), "Optional editors and locales must remain outside the actual startup module graph"); // Optional fonts must remain downloads, even when the desktop installer packages all Web files.