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.