From 12017d9fbabf40060bea5ea0b1322b1b3ed690fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=BE=D0=BB=D0=BE=D0=B2=D0=B8=D0=BD=20=D0=90=D0=BB?= =?UTF-8?q?=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20=D0=90=D0=BB=D0=B5?= =?UTF-8?q?=D0=BA=D1=81=D0=B5=D0=B5=D0=B2=D0=B8=D1=87?= Date: Fri, 11 Sep 2026 10:08:55 +0300 Subject: [PATCH 01/58] i18n(ru): complete the catalogue and repair damaged strings Complete the Russian catalogue: coverage goes from 818/1480 to 1480/1480 keys. The remaining 24 entries stay in English on purpose: the language names, the technology names, the documentation path, the product term Setup URI, the credential labels which the catalogue already keeps in English, and the strings which contain placeholders only. Repair 42 Russian strings which had lost every ${...} placeholder, so the user saw the variable names instead of the values. 13 of them had also lost text: comparison tables, callout blocks, the options of the rebuild dialogue, the generated key pair, and the QR code image. Replace the Japanese full stop with a full stop in 16 strings. The new entries follow the terminology of the current catalogue, and the accepted translations keep their wording. --- src/common/messages/combinedMessages.prod.ts | 810 ++++++++- src/common/messagesJson/ru.json | 810 ++++++++- src/common/messagesYAML/ru.yaml | 1685 +++++++++++++++--- 3 files changed, 2932 insertions(+), 373 deletions(-) diff --git a/src/common/messages/combinedMessages.prod.ts b/src/common/messages/combinedMessages.prod.ts index 0bcbaaf2..366bfca6 100644 --- a/src/common/messages/combinedMessages.prod.ts +++ b/src/common/messages/combinedMessages.prod.ts @@ -4,6 +4,7 @@ export const allMessages: Readonly [!INFO]- The connected devices have been detected as follows:\n${devices}": { @@ -200,72 +210,84 @@ export const allMessages: Readonly Storage": { def: "Database -> Storage", es: "Base de datos -> Almacenamiento", ko: "데이터베이스 -> 스토리지", + ru: "База данных -> Файлы", "zh-tw": "資料庫 -> 儲存空間", }, "Database Adapter": { @@ -1209,6 +1284,7 @@ export const allMessages: Readonly ${remote})": { def: "Higher (${local} > ${remote})", es: "Superior (${local} > ${remote})", ko: "더 높음 (${local} > ${remote})", + ru: "Больше (${local} > ${remote})", "zh-tw": "較高(${local} > ${remote})", }, "Highlight diff": { def: "Highlight diff", es: "Resaltar las diferencias", ko: "차이 강조", + ru: "Подсветить различия", "zh-tw": "醒目顯示差異", }, "How to display network errors when the sync server is unreachable.": { @@ -2581,6 +2725,7 @@ export const allMessages: Readonly [!MORE]-\n> אם אתה משתמש בפלאגין כבר שנים רבות, ייתכן שנצברו נתחים לא מקושרים — כלומר, זבל — במסד הנתונים. לכן, אנו ממליצים לבנות הכל מחדש. ככל הנראה מסד הנתונים יהיה קטן בהרבה לאחר מכן.\n>\n> אם נפח הכספת שלך פשוט גדל, עדיף לבנות מחדש לאחר ארגון הקבצים. Self-hosted LiveSync אינו מוחק נתונים בפועל גם כאשר אתה מוחק קבצים כדי להאיץ את התהליך. הדבר [מתועד בפירוט](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md).\n>\n> אם אינך מוטרד מהגידול, ניתן להגדיל את סף ההתראה ב-100MB. הדבר מתאים אם השרת הוא שלך. עם זאת, מומלץ לבנות מחדש מעת לעת.\n>\n\n> [!WARNING]\n> אם תבנה מחדש, ודא שכל המכשירים מסונכרנים. הפלאגין ינסה למזג כמה שניתן.\n", ja: "**データベースが大きくなっています!** でも心配しないでください。リモートストレージの容量が不足する前に対応できます。\n\n| 測定サイズ | 設定サイズ |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> 長年使用している場合、参照されていないチャンク(つまりゴミ)がデータベースに蓄積している可能性があります。そのため、すべてを再構築することをお勧めします。おそらくかなり小さくなるでしょう。\n>\n> 単純に保管庫の容量が増えている場合は、事前にファイルを整理してからすべてを再構築するのが良いでしょう。Self-hosted LiveSyncは処理速度を上げるため、削除しても実際のデータを削除しません。これはおおまかに[documentation](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md)に記載されています。\n>\n> 増加を気にしない場合は、通知制限を100MB単位で増やすことができます。これは自分のサーバーで実行している場合に適しています。ただし、定期的にすべてを再構築する方が良いでしょう。\n>\n\n> [!WARNING]\n> すべてを再構築する場合は、すべてのデバイスが同期されていることを確認してください。もちろん、プラグインは可能な限り解決しようと努力はしますけど...\n", ko: "**데이터베이스 용량이 점점 커지고 있습니다!** 하지만 걱정하지 마세요. 지금 대응할 수 있습니다. 원격 스토리지 공간이 부족해지기까지 남은 시간입니다.\n\n| 측정된 크기 | 설정된 한도 |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> 오랜 기간 사용했다면 참조되지 않는 청크, 즉 쓰레기 데이터가 데이터베이스에 쌓였을 수 있습니다. 이 경우 전체 재구축을 권장합니다. 용량이 훨씬 줄어들 것입니다.\n>\n> 단순히 보관함 용량이 커지고 있는 것이라면, 파일을 정리한 뒤에 전체를 재구축하는 것이 좋습니다. Self-hosted LiveSync는 처리 속도를 위해 파일을 삭제해도 실제 데이터를 바로 지우지 않습니다. 이 내용은 [기술 문서](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md)에 간략히 정리되어 있습니다.\n>\n> 용량 증가가 괜찮다면 알림 한도를 100MB 단위로 높일 수 있습니다. 직접 서버를 운영하는 경우에 적합한 방법입니다. 다만 가끔은 전체를 재구축해 주는 것이 좋습니다.\n>\n\n> [!WARNING]\n> 전체 재구축을 실행할 때는 모든 기기가 동기화되어 있는지 확인해 주세요. 플러그인이 최대한 병합하려고 시도하기는 합니다.\n", - ru: "Ваша база данных увеличивается! Но не волнуйтесь, мы можем решить это сейчас.", + ru: "**Ваша база данных увеличивается!** Но не волнуйтесь, мы можем решить это сейчас. Это время до того, как на удалённом хранилище закончится место.\n\n| Измеренный размер | Заданный размер |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> Если вы пользуетесь плагином много лет, в базе данных могли накопиться чанки без ссылок — то есть мусор. Поэтому мы рекомендуем перестроить всё. Вероятно, база станет намного меньше.\n>\n> Если объём вашего хранилища просто растёт, лучше перестроить всё после наведения порядка в файлах. Self-hosted LiveSync не удаляет сами данные, даже если вы их удалили, — так работа идёт быстрее. Это в общих чертах [описано в документации](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md).\n>\n> Если рост вас не смущает, вы можете увеличить порог уведомления на 100 МБ. Так стоит делать, если вы работаете на своём сервере. Но время от времени всё же лучше перестраивать всё.\n>\n\n> [!WARNING]\n> Если вы перестраиваете всё, убедитесь, что все устройства синхронизированы. Впрочем, плагин объединит столько, сколько сможет.\n", zh: "**您的数据库正在变大!** 但别担心,我们现在可以解决它。在远程存储空间用完之前还有时间。\n\n| 测量大小 | 配置大小 |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> 如果您已经使用了很多年,数据库中可能会积累未引用的 chunks——也就是垃圾。因此,我们建议重建所有内容。它可能会变得小得多。\n>\n> 如果您的库容量只是在增加,最好在整理文件后重建所有内容。即使您为了加速过程删除了文件,Self-hosted LiveSync 也不会删除实际数据。这大致[有文档记录](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md)。\n>\n> 如果您不介意增加,可以将通知限制增加 100MB。如果您在自己的服务器上运行,就是这种情况。但是,最好还是不时地重建所有内容。\n>\n\n> [!WARNING]\n> 如果您执行重建所有内容,请确保所有设备都已同步。尽管如此,插件会尽可能地合并\n", "zh-tw": "**你的資料庫正在變大!** 但不用擔心,我們現在就可以處理。在遠端儲存空間用盡之前還有一些時間。\n\n| 測量大小 | 設定大小 |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> 如果你已經使用了好幾年,資料庫中可能累積了未被參照的 chunks,也就是垃圾資料。因此,我們建議重建全部內容,通常會讓資料庫變得小很多。\n>\n> 如果你的 Vault 容量只是持續增加,最好先整理檔案再重建全部內容。即使你為了加快速度而刪除了檔案,Self-hosted LiveSync 也不會刪除實際資料,詳情大致記載在[文件](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md)中。\n>\n> 如果你不在意容量增加,可以將通知上限調高 100MB,這在你自行架設伺服器時較適用。不過,還是建議偶爾重建全部內容。\n>\n\n> [!WARNING]\n> 若要執行重建全部內容,請確保所有裝置都已同步。不過外掛會盡可能地進行合併。", @@ -3900,12 +4088,14 @@ export const allMessages: Readonly [!DETAILS]-\n> דגלים אלה מוגדרים על ידי הפלאגין במהלך בנייה מחדש או משיכה. אם התהליך הסתיים בצורה לא תקינה, ייתכן שהם נשארו כלא מכוון.\n> אם אינך בטוח, ניתן לנסות להריץ מחדש את התהליכים. ודא שיש לך גיבוי של הכספת.\n", ja: "Self-hosted LiveSyncは一部のイベントを無視するように設定されています。これは正しいですか?\n\n| タイプ | ステータス | メモ |\n|:---:|:---:|---|\n| ストレージイベント | ${fileWatchingStatus} | すべての変更が無視されます |\n| データベースイベント | ${parseReplicationStatus} | すべての同期された変更が延期されます |\n\nこれらを再開してObsidianを再起動しますか?\n\n> [!DETAILS]-\n> これらのフラグは、プラグインが再構築またはフェッチ中に設定されます。プロセスが異常終了した場合、意図せず保持されることがあります。\n> 不明な場合は、これらのプロセスを再実行してみてください。必ず保管庫をバックアップしてください。\n", ko: "Self-hosted LiveSync가 일부 이벤트를 무시하도록 설정되어 있습니다. 이 설정이 맞습니까?\n\n| 유형 | 상태 | 설명 |\n|:---:|:---:|---|\n| 스토리지 이벤트 | ${fileWatchingStatus} | 모든 수정 사항이 무시됩니다 |\n| 데이터베이스 이벤트 | ${parseReplicationStatus} | 모든 동기화 변경이 지연됩니다 |\n\n이벤트 감지를 다시 활성화하고 Obsidian을 재시작하시겠습니까?\n\n> [!DETAILS]-\n> 이 플래그는 플러그인이 재구축하거나 가져오는 동안 설정한 것입니다. 처리가 비정상적으로 종료되면 의도치 않게 남아 있을 수 있습니다.\n> 확실하지 않다면 해당 처리를 다시 실행해 보세요. 반드시 보관함을 백업해 두시기 바랍니다.\n", - ru: "Self-hosted LiveSync has been configured to ignore some events. Is this correct?\n\n| Type | Status | Note |\n|:---:|:---:|---|\n| Storage Events | ${fileWatchingStatus} | Every modification will be ignored |\n| Database Events | ${parseReplicationStatus} | Every synchronised change will be postponed |\n\nDo you want to resume them and restart Obsidian?\n\n> [!DETAILS]-\n> These flags are set by the plug-in while rebuilding, or fetching. If the process ends abnormally, it may be kept unintended.\n> If you are not sure, you can try to rerun these processes. Make sure to back your vault up.\n", + ru: "Self-hosted LiveSync настроен игнорировать некоторые события. Это верно?\n\n| Тип | Статус | Примечание |\n|:---:|:---:|---|\n| События хранилища | ${fileWatchingStatus} | Любое изменение будет проигнорировано |\n| События базы данных | ${parseReplicationStatus} | Любое синхронизированное изменение будет отложено |\n\nВозобновить их и перезапустить Obsidian?\n\n> [!DETAILS]-\n> Эти флаги устанавливает плагин во время перестроения или загрузки. Если процесс завершится аварийно, они могут остаться включёнными непреднамеренно.\n> Если вы не уверены, попробуйте выполнить эти процессы заново. Обязательно сделайте резервную копию хранилища.\n", zh: "Self-hosted LiveSync 已被配置为忽略某些事件。这样对吗?\n\n| 类型 | 状态 | 说明 |\n|:---:|:---:|---|\n| 存储事件 | ${fileWatchingStatus} | 所有修改都将被忽略 |\n| 数据库事件 | ${parseReplicationStatus} | 所有同步的更改都将被推迟 |\n\n您想恢复它们并重启 Obsidian 吗?\n\n> [!DETAILS]-\n> 这些标志是在重建或获取时由插件设置的。如果过程异常结束,它们可能会被无意中保留。\n> 如果您不确定,可以尝试重新运行这些过程。请确保备份您的库。\n", "zh-tw": "Self-hosted LiveSync 已設定為忽略某些事件。這樣正確嗎?\n\n| 類型 | 狀態 | 說明 |\n|:---:|:---:|---|\n| 儲存空間事件 | ${fileWatchingStatus} | 所有修改都會被忽略 |\n| 資料庫事件 | ${parseReplicationStatus} | 所有已同步的變更都會被延後 |\n\n你想要恢復它們並重新啟動 Obsidian 嗎?\n\n> [!DETAILS]-\n> 這些標記是外掛在重建或抓取時設定的。如果程序異常結束,可能會非預期地留存下來。\n> 如果你不確定,可以嘗試重新執行這些程序。請務必先備份你的 Vault。\n", @@ -4214,6 +4405,7 @@ export const allMessages: Readonly הגדר ${key} ל-${value}", ja: "CouchDB設定: ${title} -> ${key}を${value}に設定", ko: "CouchDB 구성: ${title} -> ${key}을(를) ${value}(으)로 설정", - ru: "Конфигурация CouchDB: title -> Установить key в value", + ru: "Конфигурация CouchDB: ${title} -> Установить ${key} в ${value}", zh: "CouchDB 配置:${title} -> 设置 ${key} 为 ${value}", "zh-tw": "CouchDB 設定:${title} -> 將 ${key} 設為 ${value}", }, @@ -5438,7 +5646,7 @@ export const allMessages: Readonly\nמקרא\n\n| סמל | משמעות |\n|: ------ :| ------- |\n| ⇔ | מעודכן |\n| ⇄ | סנכרן לאיזון |\n| ⇐,⇒ | העבר לדריסה |\n| ⇠,⇢ | העבר לדריסה מהצד השני |\n\n\n\n## ${OPTION_REBUILD_BOTH}\nבמבט: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nבנה מחדש גם את מסד הנתונים המקומי וגם המרוחד תוך שימוש בקבצים קיימים ממכשיר זה.\nפעולה זו תנעל מכשירים אחרים שיצטרכו לבצע משיכה.\n## ${OPTION_FETCH}\nבמבט: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nאתחל את מסד הנתונים המקומי ובנה אותו מחדש תוך שימוש בנתונים שנמשכו ממסד הנתונים המרוחד.\nכולל את המקרה שבו בנית מחדש את מסד הנתונים המרוחד.\n## ${OPTION_ONLY_SETTING}\nשמור רק את ההגדרות. **זהירות: עלול לגרום לפגיעה בנתונים**; בנייה מחדש של מסד הנתונים נדרשת בדרך כלל.", ja: "変更を適用するにはデータベースの再構築が必要です。変更を適用する方法を選択してください。\n\n
\n凡例\n\n| 記号 | 意味 |\n|: ------ :| ------- |\n| ⇔ | 最新 |\n| ⇄ | 同期してバランスを取る |\n| ⇐,⇒ | 上書きするため転送 |\n| ⇠,⇢ | 反対側から上書きするため転送 |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\n概要: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nこのデバイスの既存ファイルを使用してローカルとリモートの両方のデータベースを再構築します。\n他のデバイスはロックアウトされ、フェッチが必要です。\n## ${OPTION_FETCH}\n概要: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nローカルデータベースを初期化し、リモートデータベースから取得したデータを使用して再構築します。\nリモートデータベースを再構築した場合も含まれます。\n## ${OPTION_ONLY_SETTING}\n設定のみを保存します。**注意: データ破損につながる可能性があります**。通常、データベースの再構築が必要です。", ko: "변경 사항을 적용하려면 데이터베이스를 재구축해야 합니다. 변경 사항을 적용할 방법을 선택해 주세요.\n\n
\n범례\n\n| 기호 | 의미 |\n|: ------ :| ------- |\n| ⇔ | 최신 상태 |\n| ⇄ | 양쪽을 맞추는 동기화 |\n| ⇐,⇒ | 덮어쓰기 전송 |\n| ⇠,⇢ | 반대편에서 덮어쓰기 전송 |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\n한눈에 보기: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\n이 기기의 기존 파일을 사용해 로컬과 원격 데이터베이스를 모두 재구축합니다.\n이 경우 다른 기기는 잠기며, 가져오기를 수행해야 합니다.\n## ${OPTION_FETCH}\n한눈에 보기: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\n로컬 데이터베이스를 초기화한 뒤, 원격 데이터베이스에서 가져온 데이터로 재구축합니다.\n원격 데이터베이스를 이미 재구축한 경우도 여기에 해당합니다.\n## ${OPTION_ONLY_SETTING}\n설정만 저장합니다. **주의: 데이터가 손상될 수 있습니다.** 일반적으로는 데이터베이스 재구축이 필요합니다.", - ru: "Требуется перестроение баз данных для применения изменений.", + ru: "Требуется перестроение баз данных для применения изменений. Выберите способ применения изменений.\n\n
\nОбозначения\n\n| Символ | Значение |\n|: ------ :| ------- |\n| ⇔ | Актуально |\n| ⇄ | Синхронизация для выравнивания |\n| ⇐,⇒ | Передача с перезаписью |\n| ⇠,⇢ | Передача с перезаписью с другой стороны |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\nКоротко: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nПерестроить локальную и удалённую базы данных по файлам, которые уже есть на этом устройстве.\nДругие устройства будут заблокированы, им потребуется загрузка с удалённого.\n## ${OPTION_FETCH}\nКоротко: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nОчистить локальную базу данных и построить её заново по данным, загруженным из удалённой базы данных.\nЭтот вариант подходит и тогда, когда вы перестроили удалённую базу данных.\n## ${OPTION_ONLY_SETTING}\nСохранить только настройки. **Внимание: это может привести к повреждению данных**; обычно перестроение базы данных необходимо.", zh: "需要重建数据库以应用更改。请选择应用更改的方法。\n\n
\n图例\n\n| 符号 | 含义 |\n|: ------ :| ------- |\n| ⇔ | 最新 |\n| ⇄ | 同步以平衡 |\n| ⇐,⇒ | 传输以覆盖 |\n| ⇠,⇢ | 从另一侧传输以覆盖 |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\n概览:📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\n使用此设备的现有文件重建本地和远程数据库。\n这将导致其他设备被锁定,并且它们需要执行获取操作。\n## ${OPTION_FETCH}\n概览:📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\n初始化本地数据库并使用从远程数据库获取的数据重建它。\n这种情况包括您已经重建了远程数据库的情况。\n## ${OPTION_ONLY_SETTING}\n仅存储设置。**注意:这可能导致数据损坏**;通常需要重建数据库", "zh-tw": "需要重建資料庫才能套用變更。請選擇套用變更的方式。\n\n
\n圖例\n\n| 符號 | 含意 |\n|: ------ :| ------- |\n| ⇔ | 已是最新 |\n| ⇄ | 同步以取得一致 |\n| ⇐,⇒ | 傳輸並覆寫 |\n| ⇠,⇢ | 從另一端傳輸並覆寫 |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\n概覽: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\n以此裝置上現有的檔案重建本機與遠端資料庫。\n這會鎖定其他裝置,它們必須執行抓取。\n## ${OPTION_FETCH}\n概覽: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\n初始化本機資料庫,並以從遠端資料庫抓取的資料重建。\n這也涵蓋你已重建遠端資料庫的情況。\n## ${OPTION_ONLY_SETTING}\n僅儲存設定。**注意:這可能導致資料損毀**;通常仍需要重建資料庫。", @@ -5863,7 +6072,7 @@ export const allMessages: Readonly${to}", ja: "⚠ CORS Originが一致しません ${from}->${to}", ko: "⚠ CORS 출처가 일치하지 않습니다 ${from}->${to}", - ru: "⚠ CORS Origin не совпадает from->to", + ru: "⚠ CORS Origin не совпадает ${from}->${to}", zh: "⚠ CORS 源不匹配 {from}->{to}", "zh-tw": "⚠ CORS 來源不相符 ${from}->${to}", }, @@ -6717,12 +6933,14 @@ export const allMessages: Readonly[!FOR YOUR EYES ONLY]-\n>
\n>\n> ### מפתח ציבורי\n> ```\n${public_key}\n> ```\n>\n> ### מפתח פרטי\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Both for copying]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
\n\n', ja: 'キーペアを生成しました!\n\n注意: このキーペアは再度表示されません。安全な場所に保存してください。紛失した場合は、新しいキーペアを生成する必要があります。\n注意2: 公開鍵はspki形式、秘密鍵はpkcs8形式です。利便性のため、公開鍵の改行は`\\n`に変換されています。\n注意3: 公開鍵はリモートデータベースに、秘密鍵はローカルデバイスに設定してください。\n\n>[!FOR YOUR EYES ONLY]-\n>
\n>\n> ### 公開鍵\n> ```\n${public_key}\n> ```\n>\n> ### 秘密鍵\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Both for copying]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
\n\n', ko: '키 페어를 생성했습니다!\n\n참고: 이 키 페어는 다시 표시되지 않습니다. 안전한 곳에 저장해 주세요. 분실하면 새 키 페어를 생성해야 합니다.\n참고 2: 공개 키는 spki 형식이고, 개인 키는 pkcs8 형식입니다. 편의를 위해 공개 키의 줄 바꿈은 `\\n`으로 변환됩니다.\n참고 3: 공개 키는 원격 데이터베이스에, 개인 키는 로컬 기기에 설정해야 합니다.\n\n>[!FOR YOUR EYES ONLY]-\n>
\n>\n> ### 공개 키\n> ```\n${public_key}\n> ```\n>\n> ### 개인 키\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Both for copying]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
\n\n', - ru: "Мы сгенерировали пару ключей!", + ru: 'Мы сгенерировали пару ключей!\n\nПримечание: эта пара ключей больше никогда не будет показана. Сохраните её в надёжном месте. Если вы её потеряете, вам нужно будет сгенерировать новую пару ключей.\nПримечание 2: открытый ключ в формате spki, а закрытый ключ — в формате pkcs8. Для удобства переносы строк в открытом ключе заменены на `\\n`.\nПримечание 3: открытый ключ настраивается в удалённой базе данных, а закрытый ключ — на локальных устройствах.\n\n>[!FOR YOUR EYES ONLY]-\n>
\n>\n> ### Открытый ключ\n> ```\n${public_key}\n> ```\n>\n> ### Закрытый ключ\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Both for copying]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
\n\n', zh: '我们已经生成了一组密钥对!\n\n注意:这组密钥对之后将不会再次显示。请务必妥善保管;如果丢失,你需要重新生成新的密钥对。\n注意 2:公钥采用 spki 格式,私钥采用 pkcs8 格式。为方便复制,公钥中的换行会被转换为 `\\n`。\n注意 3:公钥应配置在远端数据库中,私钥应配置在本地设备上。\n\n>[!仅限本人查看]-\n>
\n>\n> ### 公钥\n> ```\n${public_key}\n> ```\n>\n> ### 私钥\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!整段复制]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
', "zh-tw": '我們已產生新的金鑰對!\n\n注意:此金鑰對之後將不會再次顯示。請務必妥善保存;若遺失,必須重新產生新的金鑰對。\n注意 2:公鑰採用 spki 格式,私鑰採用 pkcs8 格式。為了方便複製,公鑰中的換行會轉換為 `\\n`。\n注意 3:公鑰應設定在遠端資料庫中,私鑰則應設定在本機裝置上。\n\n>[!僅供本人查看]-\n>
\n>\n> ### 公鑰\n> ```\n${public_key}\n> ```\n>\n> ### 私鑰\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!便於整段複製]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
', @@ -8791,12 +9082,14 @@ export const allMessages: Readonly[!FOR YOUR EYES ONLY]-\n>
${qr_image}
', ja: '設定を転送するためのQRコードを生成しました。スマートフォンや他のデバイスでQRコードをスキャンしてください。\n注意: QRコードは暗号化されていないため、開く際は注意してください。\n\n>[!FOR YOUR EYES ONLY]-\n>
${qr_image}
', ko: '설정을 전송하기 위한 QR 코드를 생성했습니다. 휴대폰이나 다른 기기로 QR 코드를 스캔해 주세요.\n참고: QR 코드는 암호화되지 않았으므로 열 때 주의하세요.\n\n>[!FOR YOUR EYES ONLY]-\n>
${qr_image}
', - ru: "Мы сгенерировали QR-код для передачи настроек. Отсканируйте QR-код телефоном.", + ru: 'Мы сгенерировали QR-код для передачи настроек. Отсканируйте QR-код телефоном или другим устройством.\nВнимание: QR-код не зашифрован, открывайте его осторожно.\n\n>[!FOR YOUR EYES ONLY]-\n>
${qr_image}
', zh: 'We have generated a QR code to transfer the settings. Please scan the QR code with your phone or other device.\nNote: The QR code is not encrypted, so be careful to open this.\n\n>[!FOR YOUR EYES ONLY]-\n>
${qr_image}
', "zh-tw": '我們已產生用於傳遞設定的 QR 碼,請用你的手機或其他裝置掃描這個 QR 碼。\n注意:這個 QR 碼未經加密,開啟時請務必小心。\n\n>[!僅供本人查看]-\n>
${qr_image}
', @@ -9423,12 +9717,14 @@ export const allMessages: Readonly Database", es: "Almacenamiento -> Base de datos", ko: "스토리지 -> 데이터베이스", + ru: "Файлы -> База данных", "zh-tw": "儲存空間 -> 資料庫", }, "Strongly Recommended": { def: "Strongly Recommended", es: "Muy recomendado", ko: "적극 권장", + ru: "Настоятельно рекомендуем", "zh-tw": "強烈建議", }, "Suppress notification of hidden files change": { @@ -9679,6 +9989,7 @@ export const allMessages: Readonly[!TIP]\n> אם ברצונך לסנכרן את כל ההגדרות, אנא השתמש ב-`סנכרון הגדרות דרך Markdown` לאחר החלת תצורה מינימלית עם תכונה זו.\n\n${additionalMessage}", ja: "\nリモートデータベースの設定は以下の通りです。これらの値は、このデバイスと少なくとも1回同期された他のデバイスによって設定されています。\n\nこれらの設定を使用する場合は、設定済みの設定を使用を選択してください。\nこのデバイスの設定を維持する場合は、無視を選択してください。\n\n${table}\n\n>[!TIP]\n> すべての設定を同期したい場合は、この機能で最小限の設定を適用した後、`Sync settings via markdown`を使用してください。\n\n${additionalMessage}", ko: "\n원격 데이터베이스의 설정은 다음과 같습니다. 이 값들은 이 기기와 최소 한 번 동기화된 다른 기기에서 구성된 것입니다.\n\n이 설정을 사용하려면 구성된 설정 사용를 선택해 주세요.\n이 기기의 설정을 유지하려면 무시를 선택해 주세요.\n\n${table}\n\n>[!TIP]\n> 모든 설정을 동기화하려면 이 기능으로 최소 구성을 적용한 후 `마크다운을 통한 설정 동기화`를 사용해 주세요.\n\n${additionalMessage}", - ru: "Настройки в удалённой базе данных следующие. Эти значения настроены другими устройствами.", + ru: "\nНастройки в удалённой базе данных следующие. Эти значения настроены другими устройствами, которые хотя бы раз синхронизировались с этим устройством.\n\nЕсли вы хотите использовать эти настройки, выберите Использовать настроенные параметры.\nЕсли вы хотите сохранить настройки этого устройства, выберите Отмена.\n\n${table}\n\n>[!TIP]\n> Если вы хотите синхронизировать все настройки, используйте «Синхронизация настроек через Markdown» после применения минимальной конфигурации этой функцией.\n\n${additionalMessage}", zh: "\nThe settings in the remote database are as follows. These values are configured by other devices, which are synchronised with this device at least once.\n\nIf you want to use these settings, please select Use configured settings.\nIf you want to keep the settings of this device, please select Dismiss.\n\n${table}\n\n>[!TIP]\n> If you want to synchronise all settings, please use `Sync settings via markdown` after applying minimal configuration with this feature.\n\n${additionalMessage}", "zh-tw": "\n遠端資料庫中的設定如下。這些值由其他裝置所設定,而那些裝置至少與此裝置同步過一次。\n\n如果你想使用這些設定,請選擇「採用已設定的值」。\n如果你想保留此裝置的設定,請選擇「不處理」。\n\n${table}\n\n>[!TIP]\n> 如果你想同步所有設定,請先用此功能套用最小設定,再使用 `Sync settings via markdown`。\n\n${additionalMessage}", @@ -10291,7 +10641,7 @@ export const allMessages: Readonly Storage", es: "Base de datos -> Almacenamiento", ko: "데이터베이스 -> 스토리지", + ru: "База данных -> Файлы", zh: "数据库 -> 存储", "zh-tw": "資料庫 -> 儲存空間", }, @@ -10686,6 +11078,7 @@ export const allMessages: Readonly Database", es: "Almacenamiento -> Base de datos", ko: "스토리지 -> 데이터베이스", + ru: "Файлы -> База данных", zh: "存储 -> 数据库", "zh-tw": "儲存空間 -> 資料庫", }, @@ -10833,6 +11246,7 @@ export const allMessages: Readonly [!INFO]- The connected devices have been detected as follows:\n${devices}": "> [!INFO]- Обнаружены следующие подключённые устройства:\n${devices}", - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": "Setup URI — это одна строка текста, содержащая адрес сервера и данные аутентификации. Если URI был создан скриптом установки сервера, его использование обеспечивает простую и безопасную настройку。", + "⚠️ Important Notice": "⚠️ Важное предупреждение", + "⚠️ Please Confirm the Following": "⚠️ Подтвердите следующее", + "✔ SELECT": "✔ ВЫБРАНО", + "✔ SYNC": "✔ СИНХРОНИЗАЦИЯ", + "✔ WATCH": "✔ СЛЕЖЕНИЕ", + "📡 Off": "📡 Выкл", + "📡 On": "📡 Вкл", + "🔴 Disconnected": "🔴 Нет связи", + "🕵️ Diag": "🕵️ Диагностика", + "🗑 Delete": "🗑 Удалить", + "🟢 Connected": "🟢 Есть связь", + "${count} issue(s) detected!": "Найдено проблем: ${count}!", + "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": "Setup URI — это одна строка текста, содержащая адрес сервера и данные аутентификации. Если URI был создан скриптом установки сервера, его использование обеспечивает простую и безопасную настройку.", + "Accept": "Принять", + "Accept in session": "Принять на сеанс", + "ACCEPTED": "ПРИНЯТО", + "ACCEPTED (in session)": "ПРИНЯТО (на сеанс)", "Access Key": "Ключ доступа", + "Access Key ID": "Access Key ID", + "Action": "Действие", "Activate": "Активировать", "Active Remote Configuration": "Активная удалённая конфигурация", "Add default patterns": "Добавить шаблоны по умолчанию", "Add new connection": "Добавить подключение", + "AddOn Module (ConfigSync) has not been loaded. This is very unexpected situation. Please report this issue.": "Модуль ConfigSync не загружен. Так быть не должно. Сообщите об этой ошибке.", + "AddOn Module (HiddenFileSync) has not been loaded. This is very unexpected situation. Please report this issue.": "Модуль HiddenFileSync не загружен. Так быть не должно. Сообщите об этой ошибке.", + "Advanced": "Дополнительно", + "Advanced Settings": "Дополнительные настройки", + "After restarting, the data on this device will be uploaded to the server as the 'master copy'. Please be aware that any unintended data currently on the server will be completely overwritten.": "После перезапуска данные с этого устройства уйдут на сервер как «главная копия». Всё, что сейчас лежит на сервере, будет полностью перезаписано.", + "After restarting, the database on this device will be rebuilt using data from the server. If there are any unsynchronised files in this vault, conflicts may occur with the server data.": "После перезапуска база на этом устройстве пересоберётся из данных сервера. Если в хранилище есть несинхронизированные файлы, возможны конфликты с данными сервера.", + "After that, synchronise to a brand new vault on each other device with the new remote one by one.": "Затем по очереди синхронизируйте каждое другое устройство с новой удалённой базой в новое пустое хранилище.", + "All checks passed successfully!": "Все проверки пройдены!", "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": "У всех устройств одинаковое значение прогресса (${progress}). Похоже, ваши устройства синхронизированы, и можно продолжать Garbage Collection.", + "All the same or non-existent": "Всё одинаково или отсутствует", + "Allow in session": "Разрешить на сеанс", + "Allow permanently": "Разрешить навсегда", + "Also, please note that if you are using Peer-to-Peer synchronization, this configuration will be used when you switch to other methods and connect to a remote server in the future.": "Также обратите внимание: если вы используете синхронизацию Peer-to-Peer, эта конфигурация будет использована, когда вы позже переключитесь на другие методы и подключитесь к удалённому серверу.", "Always prompt merge conflicts": "Всегда запрашивать разрешение конфликтов слияния", "Analyse": "Анализировать", "Analyse database usage": "Анализ использования базы данных", "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": "Проанализируйте использование базы данных и создайте TSV-отчёт для самостоятельной диагностики. Полученный отчёт можно вставить в любую удобную для вас таблицу.", + "Apply All Selected": "Применить всё выбранное", "Apply Latest Change if Conflicting": "Применить последнее изменение при конфликте", "Apply preset configuration": "Применить предустановленную конфигурацию", + "Apply the settings": "Применить настройки", "Ask a passphrase at every launch": "Запрашивать парольную фразу при каждом запуске", + "Auto Connect": "Автоподключение", + "Auto Start P2P Connection": "Автозапуск соединения P2P", + "Automatic": "Автоматически", "Automatically Sync all files when opening Obsidian.": "Автоматически синхронизировать все файлы при открытии Obsidian.", + "Available Peers": "Доступные устройства", "Back": "Назад", "Back to non-configured": "Вернуть в состояние без настройки", "Batch database update": "Пакетное обновление базы данных", @@ -36,19 +82,33 @@ "Batch size": "Размер пакета", "Batch size of on-demand fetching": "Размер пакета при запросе по требованию", "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding.": "До v0.17.16 мы использовали старый адаптер для локальной базы данных. Теперь предпочтителен новый адаптер. Однако требуется перестроение локальной базы данных.", - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.", + "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": "До v0.17.16 мы использовали старый адаптер для локальной базы данных. Теперь предпочтителен новый адаптер. Однако требуется перестроение локальной базы данных. Пожалуйста, отключите этот переключатель, когда у вас будет достаточно времени. Если оставить его включённым, отключить его предложат и при загрузке из удалённой базы данных.", + "Broadcasting?": "Вещание?", "Bucket Name": "Имя бакета", + "by resetting the remote, you will be informed on other devices.": "При сбросе удалённой базы другие устройства получат уведомление.", "Cancel": "Отмена", "Cancel Garbage Collection": "Отменить Garbage Collection", + "Changing the encryption algorithm will prevent access to any data previously encrypted with a different algorithm. Ensure that all your devices are configured to use the same algorithm to maintain access to your data.": "Смена алгоритма шифрования закроет доступ к данным, зашифрованным другим алгоритмом. Задайте один и тот же алгоритм на всех устройствах, чтобы данные остались доступны.", + "Changing this setting requires migrating existing data (a bit time may be taken) and restarting Obsidian. Please make sure to back up your data before proceeding.": "Смена этого параметра требует переноса существующих данных (это займёт время) и перезапуска Obsidian. Сделайте резервную копию до начала.", "Check": "Проверить", "Check and convert non-path-obfuscated files": "Проверить и преобразовать файлы без обфускации пути", "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": "Проверяет документы, которые ещё не были преобразованы в path-obfuscated ID, и при необходимости преобразует их.", + "Check server requirements": "Проверить требования к серверу", + "Checking connection... Please wait.": "Проверяем соединение... Подождите.", + "Chunks": "Чанки", + "Close": "Закрыть", + "Close & Disconnect": "Закрыть и отключиться", + "Close this dialog": "Закрыть окно", + "Closed:": "Закрыто:", "cmdConfigSync.showCustomizationSync": "Показать синхронизацию настроек", "Comma separated `.gitignore, .dockerignore`": "Через запятую `.gitignore, .dockerignore`", + "Command": "Команда", + "Communicating": "Идёт обмен данными", "Compaction in progress on remote database...": "Выполняется компакция удалённой базы данных...", "Compaction on remote database completed successfully.": "Компакция удалённой базы данных успешно завершена.", "Compaction on remote database failed.": "Компакция удалённой базы данных завершилась ошибкой.", "Compaction on remote database timed out.": "Время ожидания компакции удалённой базы данных истекло.", + "Compare file": "Сравнить файл", "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": "Сравнивает содержимое файлов между локальной базой данных и хранилищем. Если они не совпадут, вам предложат выбрать, какую версию сохранить.", "Compatibility (Conflict Behaviour)": "Совместимость (поведение при конфликтах)", "Compatibility (Database structure)": "Совместимость (структура базы данных)", @@ -57,37 +117,60 @@ "Compatibility (Remote Database)": "Совместимость (удалённая база данных)", "Compatibility (Trouble addressed)": "Совместимость (исправленные проблемы)", "Compute revisions for chunks": "Вычислять ревизии для чанков", + "Configuration": "Настройки", "Configuration Encryption": "Шифрование конфигурации", "Configure": "Настроить", "Configure And Change Remote": "Настроить и переключить удалённое хранилище", "Configure E2EE": "Настроить сквозное шифрование", "Configure Remote": "Настроить удалённое хранилище", - "Configure the same server information as your other devices again, manually, very advanced users only.": "Снова вручную укажите те же параметры сервера, что и на других устройствах. Только для очень опытных пользователей。", + "Configure the same server information as your other devices again, manually, very advanced users only.": "Снова вручную укажите те же параметры сервера, что и на других устройствах. Только для очень опытных пользователей.", + "Connect": "Подключиться", + "Connect to existing database and continue": "Подключиться к существующей базе и продолжить", + "Connected to Signaling Server (as Peer ID: ${peerId})": "Подключено к сигнальному серверу (Peer ID: ${peerId})", + "Connected:": "Подключено:", "Connection Method": "Способ подключения", + "Connection Settings": "Настройки подключения", + "Connection:": "Соединение:", + "Continue anyway": "Всё равно продолжить", "Continue to CouchDB setup": "Перейти к настройке CouchDB", "Continue to Peer-to-Peer only setup": "Перейти к настройке только Peer-to-Peer", "Continue to S3/MinIO/R2 setup": "Перейти к настройке S3/MinIO/R2", "Copy": "Копировать", "Copy Report to clipboard": "Копировать отчёт в буфер обмена", + "CouchDB Configuration": "Настройки CouchDB", "CouchDB Connection Tweak": "Настройки подключения CouchDB", + "CouchDB validates the database name when you connect. The name must not be empty.": "CouchDB проверяет имя базы при подключении. Имя не может быть пустым.", + "Create or connect to database and continue": "Создать базу или подключиться к ней и продолжить", + "Create P2P remote": "Создать удалённую базу P2P", "Cross-platform": "Кроссплатформенные", "Current adapter: {adapter}": "Текущий адаптер: {adapter}", + "Custom Headers": "Свои заголовки", "Customization Sync": "Синхронизация настроек", "Customization Sync (Beta3)": "Синхронизация настроек (Beta3)", "Data Compression": "Сжатие данных", + "Data to Copy": "Данные для копирования", + "Database -> Storage": "База данных -> Файлы", "Database Adapter": "Адаптер базы данных", "Database Name": "Имя базы данных", "Database suffix": "Суффикс базы данных", + "Date": "Дата", "Default": "По умолчанию", "Delay conflict resolution of inactive files": "Отложить разрешение конфликтов для неактивных файлов", "Delay merge conflict prompt for inactive files.": "Отложить запрос конфликта слияния для неактивных файлов.", "Delete": "Удалить", "Delete all customization sync data": "Удалить все данные синхронизации настроек", "Delete all data on the remote server.": "Удалить все данные на удалённом сервере.", + "Delete All of": "Удалить все", "Delete local database to reset or uninstall Self-hosted LiveSync": "Удалить локальную базу данных, чтобы сбросить или удалить Self-hosted LiveSync", "Delete old metadata of deleted files on start-up": "Удалять старые метаданные удалённых файлов при запуске", "Delete Remote Configuration": "Удалить удалённую конфигурацию", "Delete remote configuration '{name}'?": "Удалить удалённую конфигурацию '{name}'?", + "Delete remote configuration '${name}'?": "Удалить настройку удалённой базы «${name}»?", + "DENIED": "ОТКАЗАНО", + "DENIED (in session)": "ОТКАЗАНО (на сеанс)", + "Deny": "Отклонить", + "Deny in session": "Отклонить на сеанс", + "Deny permanently": "Отклонить навсегда", "descConnectSetupURI": "Это рекомендуемый способ настройки Self-hosted LiveSync с помощью Setup URI.", "descCopySetupURI": "Идеально для настройки нового устройства!", "descEnableLiveSync": "Включайте это только после настройки одного из двух вариантов выше.", @@ -95,16 +178,27 @@ "descManualSetup": "Не рекомендуется, но полезно, если у вас нет Setup URI", "descTestDatabaseConnection": "Открыть подключение к базе данных.", "descValidateDatabaseConfig": "Проверяет и исправляет потенциальные проблемы с конфигурацией базы данных.", + "Deselect all": "Снять выбор со всех", "desktop": "рабочий стол", + "Detected Peers": "Обнаруженные устройства", "Developer": "Разработчик", "Device": "Устройство", + "device name": "имя устройства", "Device name": "Имя устройства", + "Device name to identify the device. Please use shorter one for the stable peer detection, i.e., \"iphone-16\" or \"macbook-2021\".": "Имя, по которому видно устройство. Берите короткое имя для надёжного обнаружения устройств, например «iphone-16» или «macbook-2021».", + "Device Peer ID": "Peer ID устройства", "Device Setup Method": "Способ настройки устройства", + "Devices:": "Устройства:", + "Diagnostic RTCPeerConnection is enabled": "Диагностика RTCPeerConnection включена", "dialog.yourLanguageAvailable": "Self-hosted LiveSync имеет переводы для вашего языка, поэтому была включена настройка языка Display language.\n\nПримечание: Не все сообщения переведены. Мы ждём ваших предложений!\nПримечание 2: При создании Issue, пожалуйста, вернитесь к lang-def, затем сделайте скриншоты, сообщения и логи. Это можно сделать в настройках.\nНадеемся, вам будет удобно использовать!", "dialog.yourLanguageAvailable.btnRevertToDefault": "Оставить lang-def", "dialog.yourLanguageAvailable.Title": "Доступен перевод!", + "Diff": "Различия", + "Different": "Отличается", "Disables all synchronization and restart.": "Отключает всю синхронизацию и перезапускает приложение.", "Disables logging, only shows notifications. Please disable if you report an issue.": "Отключает логирование, показывает только уведомления. Пожалуйста, отключите при сообщении о проблеме.", + "Disconnect": "Отключиться", + "Dismiss": "Скрыть", "Display Language": "Язык интерфейса", "Display name": "Отображаемое имя", "Do not check configuration mismatch before replication": "Не проверять несовпадение конфигурации перед репликацией", @@ -117,11 +211,11 @@ "Doctor.Button.No": "Нет", "Doctor.Button.Skip": "Оставить как есть", "Doctor.Button.Yes": "Да", - "Doctor.Dialogue.Main": "Привет! Диагностика настроек активирована из-за activateReason!\nК сожалению, некоторые настройки были обнаружены как потенциальные проблемы.\nНе волнуйтесь. Давайте решим их по очереди.\n\nСообщаем вам заранее, мы спросим о следующих пунктах.\n\nissues\n\nНачнём?", - "Doctor.Dialogue.MainFix": "name\n\n| Текущее | Идеальное |\n|:---:|:---:|\n| current | ideal |\n\n**Уровень рекомендации:** level\n\n### Почему это было обнаружено?\n\nreason\n\nnote\n\nИсправить на идеальное значение?", + "Doctor.Dialogue.Main": "Привет! Диагностика настроек активирована из-за ${activateReason}!\nК сожалению, некоторые настройки были обнаружены как потенциальные проблемы.\nНе волнуйтесь. Давайте решим их по очереди.\n\nСообщаем вам заранее, мы спросим о следующих пунктах.\n\n${issues}\n\nНачнём?", + "Doctor.Dialogue.MainFix": "\n## ${name}\n\n| Текущее | Идеальное |\n|:---:|:---:|\n| ${current} | ${ideal} |\n\n**Уровень рекомендации:** ${level}\n\n### Почему это было обнаружено?\n\n${reason}\n\n${note}\n\nИсправить на идеальное значение?", "Doctor.Dialogue.Title": "Диагностика Self-hosted LiveSync", "Doctor.Dialogue.TitleAlmostDone": "Почти готово!", - "Doctor.Dialogue.TitleFix": "Исправление проблемы current/total", + "Doctor.Dialogue.TitleFix": "Исправление проблемы ${current}/${total}", "Doctor.Level.Must": "Обязательно", "Doctor.Level.Necessary": "Необходимо", "Doctor.Level.Optional": "Опционально", @@ -131,6 +225,7 @@ "Doctor.Message.RebuildRequired": "Внимание! Для применения требуется перестроение!", "Doctor.Message.SomeSkipped": "Некоторые проблемы оставлены как есть. Спросить снова при следующем запуске?", "Doctor.RULES.E2EE_V02500.REASON": "Сквозное шифрование стало более надёжным и быстрым. Предыдущее E2EE было скомпрометировано. Следует применить как можно скорее.", + "Document History": "История документа", "Duplicate": "Дублировать", "Duplicate remote": "Дублировать удалённую конфигурацию", "E2EE Configuration": "Конфигурация сквозного шифрования", @@ -142,32 +237,71 @@ "Enable customization sync": "Включить синхронизацию настроек", "Enable Developers' Debug Tools.": "Включить инструменты разработчика.", "Enable edge case treatment features": "Включить функции обработки граничных случаев", + "Enable P2P Replicator": "Включить репликатор P2P", "Enable poweruser features": "Включить функции для опытных пользователей", "Enable this if your Object Storage doesn't support CORS": "Включите, если ваше объектное хранилище не поддерживает CORS", "Enable this option to automatically apply the most recent change to documents even when it conflicts": "Включите эту опцию для автоматического применения последних изменений к документам даже при конфликте", + "Enabled": "Включено", + "Enabling end-to-end encryption ensures that your data is encrypted on your device before being sent to the remote server. This means that even if someone gains access to the server, they won't be able to read your data without the passphrase. Make sure to remember your passphrase, as it will be required to decrypt your data on other devices.": "Сквозное шифрование шифрует данные на устройстве до отправки на удалённый сервер. Даже если кто-то получит доступ к серверу, без парольной фразы он не прочитает данные. Запомните парольную фразу: она нужна, чтобы расшифровать данные на других устройствах.", "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": "Шифровать содержимое на удалённой базе данных. Рекомендуется включить при использовании функции синхронизации плагина.", "Encrypting sensitive configuration items": "Шифрование конфиденциальных настроек", + "Encryption Algorithm": "Алгоритм шифрования", "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": "Парольная фраза шифрования. При изменении нужно перезаписать базу данных сервера новыми (зашифрованными) файлами.", "End-to-End Encryption": "Сквозное шифрование", "Endpoint URL": "URL конечной точки", "Enhance chunk size": "Улучшить размер чанка", + "Enter a complete HTTP or HTTPS URL.": "Введите полный адрес HTTP или HTTPS.", + "Enter a folder prefix (optional)": "Введите префикс папки (необязательно)", "Enter Server Information": "Ввести данные сервера", + "Enter Setup URI": "Введите Setup URI", "Enter the server information manually": "Ввести данные сервера вручную", + "Enter TURN credential": "Введите пароль TURN", + "Enter TURN username": "Введите имя пользователя TURN", + "Enter your Access Key ID": "Введите свой Access Key ID", + "Enter your Bucket Name": "Введите имя бакета", + "Enter your database name": "Введите имя базы", + "Enter your JWT Key ID": "Введите свой JWT Key ID", + "Enter your JWT secret or private key": "Введите секрет JWT или закрытый ключ", + "Enter your JWT Subject (CouchDB Username)": "Введите JWT Subject (имя пользователя CouchDB)", + "Enter your passphrase": "Введите парольную фразу", + "Enter your password": "Введите пароль", + "Enter your Region (e.g., us-east-1, auto for R2)": "Введите регион (например, us-east-1; для R2 — auto)", + "Enter your Secret Access Key": "Введите свой Secret Access Key", + "Enter your username": "Введите имя пользователя", + "Error during connection test: ${reason}": "Ошибка при проверке соединения: ${reason}", + "Error during testAndFixSettings: ${reason}": "Ошибка в testAndFixSettings: ${reason}", + "Experimental Settings": "Экспериментальные настройки", "Export": "Экспорт", "Failed to connect to remote for compaction.": "Не удалось подключиться к удалённой базе данных для компакции.", "Failed to connect to remote for compaction. ${reason}": "Не удалось подключиться к удалённой базе данных для компакции. ${reason}", + "Failed to connect to the server: ${reason}": "Не удалось подключиться к серверу: ${reason}", + "Failed to connect to the server. Please check your settings.": "Не удалось подключиться к серверу. Проверьте настройки.", + "Failed to connect to the signalling relay: ${reason}": "Не удалось подключиться к сигнальному ретранслятору: ${reason}", + "Failed to create replicator instance.": "Не удалось создать репликатор.", + "Failed to parse Setup-URI.": "Не удалось разобрать Setup-URI.", "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": "Не удалось запустить одноразовую репликацию перед Garbage Collection. Garbage Collection отменена.", "Failed to start replication after Garbage Collection.": "Не удалось запустить репликацию после Garbage Collection.", + "Failed:": "Сбоев:", "Fetch": "Получить", "Fetch chunks on demand": "Загружать чанки по требованию", "Fetch database with previous behaviour": "Загрузить базу данных с предыдущим поведением", "Fetch remote settings": "Получить настройки с удалённого хранилища", + "FETCHING": "ЗАГРУЗКА", + "Fetching status...": "Получаем состояние...", + "File integrity": "Целостность файлов", "File to resolve conflict": "Файл для разрешения конфликта", + "File to view History": "Файл для просмотра истории", "Filename": "Имя файла", - "First, please select the option that best describes your current situation.": "Сначала выберите вариант, который лучше всего описывает вашу текущую ситуацию。", + "Final Confirmation: Overwrite Server Data with This Device's Files": "Последнее подтверждение: перезаписать данные сервера файлами этого устройства", + "First, please select the option that best describes your current situation.": "Сначала выберите вариант, который лучше всего описывает вашу текущую ситуацию.", + "Fix": "Исправить", "Flag and restart": "Пометить и перезапустить", + "Flagged Selective": "Помеченные выборочно", + "Folder Prefix": "Префикс папки", + "For HS256/HS512 algorithms, provide the shared secret key. For ES256/ES512 algorithms, provide the pkcs8 PEM-formatted private key.": "Для алгоритмов HS256/HS512 укажите общий секретный ключ. Для алгоритмов ES256/ES512 укажите закрытый ключ в формате pkcs8 PEM.", "Forces the file to be synced when opened.": "Принудительно синхронизировать файл при открытии.", "Fresh Start Wipe": "Полный сброс для чистого старта", + "Furthermore, if conflicts are already present in the server data, they will be synchronised to this device as they are, and you will need to resolve them locally.": "Кроме того, конфликты, которые уже есть на сервере, придут на это устройство как есть, и разбирать их придётся здесь.", "Garbage Collection cancelled by user.": "Пользователь отменил Garbage Collection.", "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": "Garbage Collection завершена. Удалено чанков: ${deletedChunks} / ${totalChunks}. Затраченное время: ${seconds} сек.", "Garbage Collection Confirmation": "Подтверждение Garbage Collection", @@ -175,16 +309,37 @@ "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": "Garbage Collection: найдено ${unusedChunks} неиспользуемых чанков для удаления.", "Garbage Collection: Scanned ${scanned} / ~${docCount}": "Garbage Collection: просканировано ${scanned} / ~${docCount}", "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": "Garbage Collection: сканирование завершено. Всего чанков: ${totalChunks}, используемых чанков: ${usedChunks}", + "Gathering information...": "Собираем сведения...", + "Generate Random ID": "Создать случайный ID", + "Group ID": "ID группы", "Handle files as Case-Sensitive": "Обрабатывать файлы с учётом регистра", + "Have you created a backup before proceeding?": "Вы сделали резервную копию перед началом?", + "Hidden file synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.": "Синхронизация скрытых файлов временно выключена. Включите её после того, как заберёте данные с сервера, если она вам нужна.", "Hidden Files": "Скрытые файлы", + "Hide completely": "Скрыть полностью", + "Hide not applicable items": "Скрыть неподходящие пункты", + "Hide password": "Скрыть пароль", + "Higher (${local} > ${remote})": "Больше (${local} > ${remote})", + "Highlight diff": "Подсветить различия", "How to display network errors when the sync server is unreachable.": "Определяет, как отображать сетевые ошибки, если сервер синхронизации недоступен.", "How would you like to configure the connection to your server?": "Как вы хотите настроить подключение к серверу?", + "However, This should not be enabled if you want to increase your secrecy more.": "Но если вам нужна повышенная скрытность, не включайте это.", "I am adding a device to an existing synchronisation setup": "Я добавляю устройство к существующей настройке синхронизации", "I am setting this up for the first time": "Я настраиваю это впервые", + "I am setting up a new server for the first time / I want to reset my existing server.": "Я настраиваю новый сервер впервые / хочу сбросить существующий сервер.", + "I am unable to create a backup of my Vault.": "Я не могу сделать резервную копию хранилища.", + "I am unable to create a backup of my Vaults.": "Я не могу сделать резервные копии хранилищ.", + "I have created a backup of my Vault.": "Я сделал резервную копию хранилища.", "I know my server details, let me enter them": "Я знаю параметры сервера, позвольте ввести их вручную", + "I understand that all changes made on other smartphones or computers possibly could be lost.": "Я понимаю, что изменения на других смартфонах и компьютерах могут пропасть.", + "I understand that other devices will no longer be able to synchronise, and will need to be reset the synchronisation information.": "Я понимаю, что другие устройства перестанут синхронизироваться и на них надо будет сбросить данные синхронизации.", + "I understand that this action is irreversible once performed.": "Я понимаю, что это действие нельзя отменить.", + "I understand the risks and will proceed without a backup.": "Я понимаю риски и продолжу без резервной копии.", + "I Understand, Overwrite Server": "Понимаю, перезаписать сервер", + "If \"Auto Start P2P Connection\" is enabled, the P2P connection will be started automatically when the plug-in launches.": "Если включён параметр «Автозапуск соединения P2P», соединение P2P запускается при старте плагина.", "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": "Если отключено, чанки будут разделяться в основном потоке.", "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this.": "Если включено, будет использоваться эффективная синхронизация настроек для каждого файла.", - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.", + "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": "Если включено, будет использоваться эффективная синхронизация настроек для каждого файла. При включении потребуется небольшая миграция. Все устройства должны быть обновлены до v0.23.18. После включения совместимость со старыми версиями будет потеряна.", "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": "Если включено, чанки будут разделены не более чем на 100 элементов.", "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": "Если включено, вновь созданные чанки временно хранятся в документе.", "If enabled, the ⛔ icon will be shown inside the status instead of the file warnings banner. No details will be shown.": "Если включено, значок будет показан внутри статуса.", @@ -194,21 +349,46 @@ "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": "Если включено, все файлы обрабатываются с учётом регистра.", "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": "Если включено, чанки будут разделены на семантически значимые сегменты.", "If this is set, changes to local files which are matched by the ignore files will be skipped.": "Если установлено, изменения файлов из списка игнорирования будут пропущены.", - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.", + "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": "Если установлено, изменения файлов из списка игнорирования будут пропущены. Удалённые изменения определяются по локальным файлам игнорирования.", "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely.": "Если эта опция включена, PouchDB будет держать соединение открытым 60 секунд.", - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.", + "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": "Если эта опция включена, PouchDB будет держать соединение открытым 60 секунд и, если за это время не придёт ни одного изменения, закроет и откроет сокет заново, вместо того чтобы держать соединение открытым бесконечно. Полезно, когда прокси ограничивает длительность запроса, но может увеличить расход ресурсов.", + "If you cannot avoid CORS issues, you might want to try this option. It uses Obsidian's internal API to communicate with the CouchDB server. Not compliant with web standards, but works. Note that this might break in future Obsidian versions.": "Если не удаётся обойти проблемы CORS, попробуйте этот параметр. Он использует внутренний API Obsidian для связи с сервером CouchDB. Это не по веб-стандартам, но работает. Учтите: в новых версиях Obsidian это может перестать работать.", + "If you cannot avoid CORS issues, you might want to try this option. It uses Obsidian's internal API to communicate with the S3 server. Not compliant with web standards, but works. Note that this might break in future Obsidian versions.": "Если не удаётся обойти проблемы CORS, попробуйте этот параметр. Он использует внутренний API Obsidian для связи с сервером S3. Это не по веб-стандартам, но работает. Учтите: в новых версиях Obsidian это может перестать работать.", + "If you have unsynchronised changes in your Vault on this device, they will likely diverge from the server's versions after the reset. This may result in a large number of file conflicts.": "Если в хранилище на этом устройстве есть несинхронизированные изменения, после сброса они разойдутся с версиями на сервере. Тогда появится много конфликтов файлов.", + "If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value.": "Если при работе с IBM Cloudant вы упёрлись в предел размера запроса, уменьшите размер пакета и лимит пакета.", + "If you understand the risks and still wish to proceed, select so.": "Если вы понимаете риски и всё равно хотите продолжить, выберите этот вариант.", + "If you want to store the data in a specific folder within the bucket, you can specify a folder prefix here. Otherwise, leave it blank to store data at the root of the bucket.": "Чтобы данные лежали в отдельной папке внутри бакета, укажите здесь префикс папки. Оставьте поле пустым, и данные лягут в корень бакета.", + "If you want to use `LiveSync`, you should broadcast changes. All `watching` peers which detects this will start the replication for fetching.": "Для работы `LiveSync` включите вещание изменений. Все устройства в режиме `watching` заметят это и запустят репликацию, чтобы забрать данные с сервера.", + "Ignore": "Пропустить", "Ignore and Proceed": "Игнорировать и продолжить", "Ignore files": "Файлы для игнорирования", "Ignore patterns": "Шаблоны исключения", "Import connection": "Импортировать подключение", + "IMPORTANT": "ВАЖНО", + "In most cases, you should stick with the default algorithm (${algorithm}), This setting is only required if you have an existing Vault encrypted in a different format.": "В большинстве случаев оставьте алгоритм по умолчанию (${algorithm}). Этот параметр нужен, только если у вас уже есть хранилище, зашифрованное в другом формате.", + "In this scenario, Self-hosted LiveSync will recreate metadata for every file and deliberately generate conflicts. Where the file content is identical, these conflicts will be resolved automatically.": "В этом случае Self-hosted LiveSync заново создаст метаданные для каждого файла и намеренно создаст конфликты. Там, где содержимое файлов совпадает, конфликты решатся сами.", + "Incoming:": "Входящие:", "Incubate Chunks in Document": "Инкубировать чанки в документе", + "Initial Action": "Первое действие", "Initialise all journal history, On the next sync, every item will be received and sent.": "Инициализирует всю историю журнала. При следующей синхронизации каждый элемент будет заново получен и отправлен.", + "Initialise journal received history. On the next sync, every item except this device sent will be downloaded again.": "Обнуляет историю полученных записей журнала. При следующей синхронизации всё, кроме отправленного этим устройством, будет загружено заново.", + "Initialise journal sent history. On the next sync, every item except this device received will be sent again.": "Обнуляет историю отправленных записей журнала. При следующей синхронизации всё, кроме полученного этим устройством, будет отправлено заново.", "Interval (sec)": "Интервал (сек)", + "INVERTED": "ИНВЕРТИРОВАНО", + "Issue detection log:": "Журнал поиска проблем:", + "It is strongly advised to create a backup before proceeding. Continuing without a backup may lead to data loss.": "Настоятельно советуем сделать резервную копию. Без неё вы можете потерять данные.", + "Just for a minute, please!": "Одну минуту!", + "JWT (JSON Web Token) authentication allows you to securely authenticate with the CouchDB server using tokens. Ensure that your CouchDB server is configured to accept JWTs and that the provided key and settings match the server's configuration. Incidentally, I have not verified it very thoroughly.": "Аутентификация JWT (JSON Web Token) позволяет безопасно входить на сервер CouchDB по токенам. Проверьте, что сервер CouchDB принимает JWT и что ключ и настройки совпадают с настройками сервера. К слову, это проверено не очень тщательно.", + "JWT Algorithm": "Алгоритм JWT", + "JWT Expiration Duration (minutes)": "Срок жизни JWT (минуты)", + "JWT Key": "Ключ JWT", + "JWT Key ID (kid)": "JWT Key ID (kid)", + "JWT Subject (sub)": "JWT Subject (sub)", "K.exp": "Экспериментальная", "K.long_p2p_sync": "title_p2p_sync", "K.P2P": "Peer-к-Peer", "K.Peer": "Устройство", - "K.ScanCustomization": "Scan customization", + "K.ScanCustomization": "Сканировать настройки", "K.short_p2p_sync": "P2P Синхр.", "K.title_p2p_sync": "Синхронизация между устройствами", "Keep empty folder": "Сохранять пустые папки", @@ -217,6 +397,7 @@ "lang-def": "lang_def", "lang-es": "Español", "lang-fr": "Français", + "lang-he": "Иврит", "lang-ja": "日本語", "lang-ko": "한국어", "lang-ru": "Русский", @@ -224,19 +405,21 @@ "lang-zh-tw": "繁體中文", "Later": "Позже", "Limit: {datetime} ({timestamp})": "Лимит: {datetime} ({timestamp})", + "live": "живая синхронизация", "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": "LiveSync не может обработать несколько хранилищ с одинаковым именем без разных префиксов.", "liveSyncReplicator.beforeLiveSync": "Перед LiveSync запускаем OneShot...", "liveSyncReplicator.cantReplicateLowerValue": "Нельзя реплицировать с меньшим значением.", "liveSyncReplicator.checkingLastSyncPoint": "Поиск последней точки синхронизации.", - "liveSyncReplicator.couldNotConnectTo": "Не удалось подключиться к uri : name\n(db)", - "liveSyncReplicator.couldNotConnectToRemoteDb": "Не удалось подключиться к удалённой базе данных: d", + "liveSyncReplicator.couldNotConnectTo": "Не удалось подключиться к ${uri} : ${name}\n(${db})", + "liveSyncReplicator.couldNotConnectToRemoteDb": "Не удалось подключиться к удалённой базе данных: ${d}", "liveSyncReplicator.couldNotConnectToServer": "Не удалось подключиться к серверу.", - "liveSyncReplicator.couldNotConnectToURI": "Не удалось подключиться к uri:dbRet", + "liveSyncReplicator.couldNotConnectToURI": "Не удалось подключиться к ${uri}:${dbRet}", "liveSyncReplicator.couldNotMarkResolveRemoteDb": "Не удалось отметить удалённую базу данных как разрешённую.", "liveSyncReplicator.liveSyncBegin": "Начало LiveSync...", "liveSyncReplicator.lockRemoteDb": "Блокировка удалённой базы данных для предотвращения повреждения данных", "liveSyncReplicator.markDeviceResolved": "Отметить это устройство как «разрешённое».", - "liveSyncReplicator.oneShotSyncBegin": "Начало OneShot синхронизации... (syncMode)", + "liveSyncReplicator.mismatchedTweakDetected": "Настройки на устройствах разошлись. Запустите репликацию вручную — плагин попробует это исправить.", + "liveSyncReplicator.oneShotSyncBegin": "Начало OneShot синхронизации... (${syncMode})", "liveSyncReplicator.remoteDbCorrupted": "Удалённая база данных новее или повреждена, убедитесь, что установлена последняя версия self-hosted-livesync", "liveSyncReplicator.remoteDbCreatedOrConnected": "Удалённая база данных создана или подключена", "liveSyncReplicator.remoteDbDestroyed": "Удалённая база данных уничтожена", @@ -244,13 +427,14 @@ "liveSyncReplicator.remoteDbMarkedResolved": "Удалённая база данных отмечена как разрешённая.", "liveSyncReplicator.replicationClosed": "Репликация закрыта", "liveSyncReplicator.replicationInProgress": "Репликация уже выполняется", - "liveSyncReplicator.retryLowerBatchSize": "Повтор с меньшим размером пакета: batch_size/batches_limit", + "liveSyncReplicator.retryLowerBatchSize": "Повтор с меньшим размером пакета: ${batch_size}/${batches_limit}", "liveSyncReplicator.unlockRemoteDb": "Разблокировка удалённой базы данных для предотвращения повреждения данных", - "liveSyncSetting.errorNoSuchSettingItem": "Такого параметра настройки не существует: key", - "liveSyncSetting.originalValue": "Оригинал: value", - "liveSyncSetting.valueShouldBeInRange": "Значение должно быть min < значение < max", + "liveSyncSetting.errorNoSuchSettingItem": "Такого параметра настройки не существует: ${key}", + "liveSyncSetting.originalValue": "Оригинал: ${value}", + "liveSyncSetting.valueShouldBeInRange": "Значение должно быть ${min} < значение < ${max}", "liveSyncSettings.btnApply": "Применить", "Local Database Tweak": "Настройки локальной базы данных", + "Local only": "Только локально", "Lock": "Заблокировать", "Lock Server": "Заблокировать сервер", "Lock the remote server to prevent synchronization with other devices.": "Блокирует удалённый сервер, чтобы запретить синхронизацию с другими устройствами.", @@ -259,6 +443,9 @@ "logPane.pause": "Пауза", "logPane.title": "Лог Self-hosted LiveSync", "logPane.wrap": "Перенос", + "Lower (${local} < ${remote})": "Меньше (${local} < ${remote})", + "Maintenance Commands": "Команды обслуживания", + "Maintenance mode": "Режим обслуживания", "Maximum delay for batch database updating": "Максимальная задержка пакетного обновления базы данных", "Maximum file size": "Максимальный размер файла", "Maximum Incubating Chunk Size": "Максимальный размер инкубируемого чанка", @@ -271,20 +458,24 @@ "Merge": "Объединить", "Minimum delay for batch database updating": "Минимальная задержка пакетного обновления базы данных", "Minimum interval for syncing": "Минимальный интервал синхронизации", + "Mixed": "Смешано", "moduleCheckRemoteSize.logCheckingStorageSizes": "Проверка размеров хранилища", - "moduleCheckRemoteSize.logCurrentStorageSize": "Размер удалённого хранилища: measuredSize", - "moduleCheckRemoteSize.logExceededWarning": "Размер удалённого хранилища: measuredSize превысил notifySize", - "moduleCheckRemoteSize.logThresholdEnlarged": "Порог увеличен до sizeМБ", + "moduleCheckRemoteSize.logCurrentStorageSize": "Размер удалённого хранилища: ${measuredSize}", + "moduleCheckRemoteSize.logExceededWarning": "Размер удалённого хранилища: ${measuredSize} превысил ${notifySize}", + "moduleCheckRemoteSize.logThresholdEnlarged": "Порог увеличен до ${size} МБ", "moduleCheckRemoteSize.msgConfirmRebuild": "Это может занять некоторое время. Вы действительно хотите перестроить всё сейчас?", - "moduleCheckRemoteSize.msgDatabaseGrowing": "Ваша база данных увеличивается! Но не волнуйтесь, мы можем решить это сейчас.", + "moduleCheckRemoteSize.msgDatabaseGrowing": "**Ваша база данных увеличивается!** Но не волнуйтесь, мы можем решить это сейчас. Это время до того, как на удалённом хранилище закончится место.\n\n| Измеренный размер | Заданный размер |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> Если вы пользуетесь плагином много лет, в базе данных могли накопиться чанки без ссылок — то есть мусор. Поэтому мы рекомендуем перестроить всё. Вероятно, база станет намного меньше.\n>\n> Если объём вашего хранилища просто растёт, лучше перестроить всё после наведения порядка в файлах. Self-hosted LiveSync не удаляет сами данные, даже если вы их удалили, — так работа идёт быстрее. Это в общих чертах [описано в документации](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md).\n>\n> Если рост вас не смущает, вы можете увеличить порог уведомления на 100 МБ. Так стоит делать, если вы работаете на своём сервере. Но время от времени всё же лучше перестраивать всё.\n>\n\n> [!WARNING]\n> Если вы перестраиваете всё, убедитесь, что все устройства синхронизированы. Впрочем, плагин объединит столько, сколько сможет.\n", "moduleCheckRemoteSize.msgSetDBCapacity": "Можно установить предупреждение о максимальной ёмкости базы данных.", + "moduleCheckRemoteSize.noticeExceeded": "Размер удалённого хранилища ${measuredSize}, это больше заданного порога уведомления ${notifySize}. {HERE}", + "moduleCheckRemoteSize.noticeNotConfigured": "Уведомления о размере удалённого хранилища не настроены. {HERE}", "moduleCheckRemoteSize.option2GB": "2ГБ (Стандарт)", "moduleCheckRemoteSize.option800MB": "800МБ (Cloudant, fly.io)", "moduleCheckRemoteSize.optionAskMeLater": "Спросить позже", "moduleCheckRemoteSize.optionDismiss": "Отклонить", - "moduleCheckRemoteSize.optionIncreaseLimit": "увеличить до newMaxМБ", + "moduleCheckRemoteSize.optionIncreaseLimit": "увеличить до ${newMax} МБ", "moduleCheckRemoteSize.optionNoWarn": "Нет, не уведомлять", "moduleCheckRemoteSize.optionRebuildAll": "Перестроить всё сейчас", + "moduleCheckRemoteSize.optionReview": "Посмотреть варианты", "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": "Размер удалённого хранилища превысил лимит", "moduleCheckRemoteSize.titleDatabaseSizeNotify": "Настройка уведомления о размере базы данных", "moduleInputUIObsidian.defaultTitleConfirmation": "Подтверждение", @@ -294,23 +485,24 @@ "moduleLiveSyncMain.logAdditionalSafetyScan": "Дополнительная проверка безопасности...", "moduleLiveSyncMain.logLoadingPlugin": "Загрузка плагина...", "moduleLiveSyncMain.logPluginInitCancelled": "Инициализация плагина отменена модулем", - "moduleLiveSyncMain.logPluginVersion": "Self-hosted LiveSync vmanifestVersion packageVersion", + "moduleLiveSyncMain.logPluginVersion": "Self-hosted LiveSync v${manifestVersion} ${packageVersion}", "moduleLiveSyncMain.logReadChangelog": "LiveSync обновлён, пожалуйста, прочитайте список изменений!", "moduleLiveSyncMain.logSafetyScanCompleted": "Дополнительная проверка безопасности завершена", "moduleLiveSyncMain.logSafetyScanFailed": "Дополнительная проверка безопасности не удалась в модуле", "moduleLiveSyncMain.logUnloadingPlugin": "Выгрузка плагина...", "moduleLiveSyncMain.logVersionUpdate": "LiveSync обновлён. В случае критических изменений автоматическая синхронизация временно отключена. Убедитесь, что все устройства обновлены перед включением.", - "moduleLiveSyncMain.msgScramEnabled": "Self-hosted LiveSync has been configured to ignore some events. Is this correct?\n\n| Type | Status | Note |\n|:---:|:---:|---|\n| Storage Events | ${fileWatchingStatus} | Every modification will be ignored |\n| Database Events | ${parseReplicationStatus} | Every synchronised change will be postponed |\n\nDo you want to resume them and restart Obsidian?\n\n> [!DETAILS]-\n> These flags are set by the plug-in while rebuilding, or fetching. If the process ends abnormally, it may be kept unintended.\n> If you are not sure, you can try to rerun these processes. Make sure to back your vault up.\n", + "moduleLiveSyncMain.msgScramEnabled": "Self-hosted LiveSync настроен игнорировать некоторые события. Это верно?\n\n| Тип | Статус | Примечание |\n|:---:|:---:|---|\n| События хранилища | ${fileWatchingStatus} | Любое изменение будет проигнорировано |\n| События базы данных | ${parseReplicationStatus} | Любое синхронизированное изменение будет отложено |\n\nВозобновить их и перезапустить Obsidian?\n\n> [!DETAILS]-\n> Эти флаги устанавливает плагин во время перестроения или загрузки. Если процесс завершится аварийно, они могут остаться включёнными непреднамеренно.\n> Если вы не уверены, попробуйте выполнить эти процессы заново. Обязательно сделайте резервную копию хранилища.\n", "moduleLiveSyncMain.optionKeepLiveSyncDisabled": "Оставить LiveSync отключённым", "moduleLiveSyncMain.optionResumeAndRestart": "Продолжить и перезапустить Obsidian", "moduleLiveSyncMain.titleScramEnabled": "Экстренная остановка включена", "moduleLocalDatabase.logWaitingForReady": "Ожидание готовности...", + "moduleLog.pathComponentTooLong": "В этом пути есть имя файла или папки длиннее ${maxBytes} байт UTF-8. На некоторых файловых системах Android и Linux оно может не работать.", "moduleLog.showLog": "Показать лог", "moduleMigration.fix0256.buttons.checkItLater": "Проверить позже", "moduleMigration.fix0256.buttons.DismissForever": "Исправлено, больше не спрашивать", "moduleMigration.fix0256.buttons.fix": "Исправить", - "moduleMigration.fix0256.message": "Из-за недавней ошибки некоторые файлы могут быть неправильно сохранены.", - "moduleMigration.fix0256.messageUnrecoverable": "Файлы не могут быть исправлены на этом устройстве:", + "moduleMigration.fix0256.message": "Из-за недавней ошибки (в v0.25.6) некоторые файлы могли быть неправильно сохранены в базе данных синхронизации.\nМы проверили файлы и нашли те, которые нужно исправить.\n\n**Файлы, готовые к исправлению:**\n\n${files}\n\nУ этих файлов в хранилище есть исходный файл совпадающего размера, поэтому они, скорее всего, восстановимы.\nМы можем использовать их, чтобы исправить базу данных. Нажмите кнопку «Исправить» ниже.\n\n${messageUnrecoverable}\n\nЕсли вы захотите запустить это снова, вы можете сделать это из Hatch.\n", + "moduleMigration.fix0256.messageUnrecoverable": "**Файлы не могут быть исправлены на этом устройстве:**\n\n${filesNotRecoverable}\n\nУ этих файлов несогласованные метаданные, и на этом устройстве их исправить нельзя (чаще всего мы не можем определить, какие данные верны).\nЧтобы восстановить их, проверьте другие свои устройства (тоже этой функцией) или восстановите файлы вручную из резервной копии.\n", "moduleMigration.fix0256.title": "Обнаружены повреждённые файлы", "moduleMigration.insecureChunkExist.buttons.fetch": "Я уже перестроил удалённую. Загрузить с удалённой", "moduleMigration.insecureChunkExist.buttons.later": "Сделаю позже", @@ -321,13 +513,13 @@ "moduleMigration.logBulkSendCorrupted": "Отправка чанков пакетами была включена, но эта функция была повреждена. Приносим извинения. Автоматически отключено.", "moduleMigration.logFetchRemoteTweakFailed": "Не удалось загрузить удалённые настройки", "moduleMigration.logLocalDatabaseNotReady": "Что-то пошло не так! Локальная база данных не готова", - "moduleMigration.logMigratedSameBehaviour": "Миграция на db:current с тем же поведением, что и раньше", - "moduleMigration.logMigrationFailed": "Миграция не удалась или отменена с old на current", + "moduleMigration.logMigratedSameBehaviour": "Миграция на db:${current} с тем же поведением, что и раньше", + "moduleMigration.logMigrationFailed": "Миграция не удалась или отменена с ${old} на ${current}", "moduleMigration.logRedflag2CreationFail": "Не удалось создать redflag2", "moduleMigration.logRemoteTweakUnavailable": "Не удалось получить удалённые настройки", "moduleMigration.logSetupCancelled": "Настройка отменена, Self-hosted LiveSync ожидает вашей настройки!", "moduleMigration.msgFetchRemoteAgain": "Удалённая база данных, похоже, уже была мигрирована. Конфигурация этого устройства несовместима.", - "moduleMigration.msgSinceV02321": "Начиная с v0.23.21, self-hosted LiveSync изменил поведение и структуру базы данных.", + "moduleMigration.msgSinceV02321": "Начиная с v0.23.21, self-hosted LiveSync изменил поведение по умолчанию и структуру базы данных. Внесены следующие изменения:\n\n1. **Чувствительность имён файлов к регистру**\n Теперь имена файлов обрабатываются без учёта регистра. Для большинства платформ это полезное изменение, кроме Linux и iOS, которые плохо работают с регистром имён файлов.\n (На них для файлов с одинаковым именем в разном регистре будет показано предупреждение).\n\n2. **Обработка ревизий чанков**\n Чанки неизменяемы, поэтому их ревизии можно зафиксировать. Это изменение ускорит сохранение файлов.\n\n___Однако, чтобы включить любое из этих изменений, нужно перестроить и удалённую, и локальную базы данных. Это занимает несколько минут, и мы рекомендуем делать это, когда у вас есть достаточно времени.___\n\n- Если вы хотите сохранить прежнее поведение, вы можете пропустить этот процесс кнопкой `${KEEP}`.\n- Если времени не хватает, выберите `${DISMISS}`. Мы спросим вас позже.\n- Если вы уже перестроили базу данных на другом устройстве, выберите `${DISMISS}` и попробуйте синхронизироваться снова. Расхождение обнаружено, поэтому вопрос появится снова.", "moduleMigration.optionAdjustRemote": "Настроить под удалённую", "moduleMigration.optionDecideLater": "Решить позже", "moduleMigration.optionEnableBoth": "Включить оба", @@ -339,24 +531,40 @@ "moduleMigration.titleCaseSensitivity": "Чувствительность к регистру", "moduleObsidianMenu.replicate": "Реплицировать", "More actions": "Другие действия", + "Mostly Complete: Decision Required": "Почти готово: нужно ваше решение", "Move remotely deleted files to the trash, instead of deleting.": "Перемещать удалённые на удалённом сервере файлы в корзину вместо удаления.", + "My remote server is already set up. I want to join this device.": "Мой удалённый сервер уже настроен. Хочу подключить это устройство.", + "Name": "Имя", "Network warning style": "Стиль сетевого предупреждения", + "NEW": "НОВОЕ", "New Remote": "Новое удалённое хранилище", + "Newer (${diff})": "Новее (${diff})", + "No checks have been performed yet.": "Проверки ещё не выполнялись.", "No connected device information found. Cancelling Garbage Collection.": "Не найдена информация о подключённых устройствах. Garbage Collection отменяется.", + "No Connection": "Нет соединения", + "No devices available. Waiting for other devices to connect...": "Устройств нет. Ждём подключения других устройств...", + "No Items.": "Записей нет.", "No limit configured": "Лимит не задан", + "NO PREVIEW": "НЕТ ПРЕДПРОСМОТРА", "No, please take me back": "Нет, верните меня назад", "Node ID": "ID узла", "Node Information Missing": "Отсутствует информация об узле", "Non-Synchronising files": "Несинхронизируемые файлы", "Normal Files": "Обычные файлы", "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": "Не все сообщения переведены. И, пожалуйста, вернитесь к «По умолчанию» при сообщении об ошибках.", + "Not configured": "Не настроено", + "Not now": "Не сейчас", + "Note that the Group ID is not limited to the generated format; you can use any string as the Group ID.": "ID группы не обязан совпадать с созданным форматом: подойдёт любая строка.", + "Note that you can generate a new Setup URI by running the \"Copy settings as a new Setup URI\" command in the command palette.": "Новый Setup URI можно создать, выполнив в палитре команд команду «Скопировать настройки как новый Setup URI».", "Notify all setting files": "Уведомлять обо всех файлах настроек", "Notify customized": "Уведомлять о настройках", "Notify when other device has newly customized.": "Уведомлять, когда другое устройство изменило настройки.", "Notify when the estimated remote storage size exceeds on start up": "Уведомлять, когда оценочный размер удалённого хранилища превышает при запуске", "Number of batches to process at a time. Defaults to 40. Minimum is 2.": "Количество пакетов для обработки за раз. По умолчанию 40. Минимум 2.", - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.", + "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": "Количество пакетов для обработки за раз. По умолчанию 40. Минимум 2. Вместе с размером пакета это определяет, сколько документов одновременно хранится в памяти.", "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": "Количество изменений для синхронизации за раз. По умолчанию 50. Минимум 2.", + "Obfuscate Properties": "Скрывать свойства файлов", + "Obfuscating properties (e.g., path of file, size, creation and modification dates) adds an additional layer of security by making it harder to identify the structure and names of your files and folders on the remote server. This helps protect your privacy and makes it more difficult for unauthorized users to infer information about your data.": "Скрытие свойств (путь к файлу, размер, даты создания и изменения) добавляет ещё один уровень защиты: по данным на сервере труднее понять структуру и имена ваших файлов и папок. Это бережёт вашу личную информацию и мешает посторонним узнать что-то о ваших данных.", "Obsidian version": "Версия Obsidian", "obsidianLiveSyncSettingTab.btnApply": "Применить", "obsidianLiveSyncSettingTab.btnCheck": "Проверить", @@ -403,29 +611,30 @@ "obsidianLiveSyncSettingTab.logCheckingConfigDone": "Проверка конфигурации завершена", "obsidianLiveSyncSettingTab.logCheckingConfigFailed": "Проверка конфигурации не удалась", "obsidianLiveSyncSettingTab.logCheckingDbConfig": "Проверка конфигурации базы данных", - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": "ОШИБКА: Не удалось проверить пароль с удалённым сервером: db.", + "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": "ОШИБКА: Не удалось проверить парольную фразу с удалённым сервером:\n${db}.", "obsidianLiveSyncSettingTab.logConfiguredDisabled": "Настроенный режим синхронизации: ОТКЛЮЧЕН", "obsidianLiveSyncSettingTab.logConfiguredLiveSync": "Настроенный режим синхронизации: LiveSync", "obsidianLiveSyncSettingTab.logConfiguredPeriodic": "Настроенный режим синхронизации: Периодический", - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": "Конфигурация CouchDB: title не удалась", - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": "Конфигурация CouchDB: title -> Установить key в value", - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": "Конфигурация CouchDB: title успешно обновлена", + "obsidianLiveSyncSettingTab.logCouchDbConfigFail": "Конфигурация CouchDB: ${title} не удалась", + "obsidianLiveSyncSettingTab.logCouchDbConfigSet": "Конфигурация CouchDB: ${title} -> Установить ${key} в ${value}", + "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": "Конфигурация CouchDB: ${title} успешно обновлена", "obsidianLiveSyncSettingTab.logDatabaseConnected": "База данных подключена", "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": "Вы не можете включить шифрование без парольной фразы", "obsidianLiveSyncSettingTab.logEncryptionNoSupport": "Ваше устройство не поддерживает шифрование.", "obsidianLiveSyncSettingTab.logErrorOccurred": "Произошла ошибка!!", - "obsidianLiveSyncSettingTab.logEstimatedSize": "Примерный размер: size", + "obsidianLiveSyncSettingTab.logEstimatedSize": "Примерный размер: ${size}", "obsidianLiveSyncSettingTab.logPassphraseInvalid": "Парольная фраза недействительна, пожалуйста, исправьте.", "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": "ОШИБКА: Парольная фраза несовместима с удалённым сервером!", "obsidianLiveSyncSettingTab.logRebuildNote": "Синхронизация отключена, загрузите и включите снова при желании.", "obsidianLiveSyncSettingTab.logSelectAnyPreset": "Выберите любой пресет.", + "obsidianLiveSyncSettingTab.logServerConfigurationCheck": "obsidianLiveSyncSettingTab.logServerConfigurationCheck", "obsidianLiveSyncSettingTab.msgAreYouSureProceed": "Вы уверены, что хотите продолжить?", "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": "Изменения нужно применить!", "obsidianLiveSyncSettingTab.msgConfigCheck": "--Проверка конфигурации--", "obsidianLiveSyncSettingTab.msgConfigCheckFailed": "Проверка конфигурации не удалась. Вы всё равно хотите продолжить?", "obsidianLiveSyncSettingTab.msgConnectionCheck": "--Проверка подключения--", "obsidianLiveSyncSettingTab.msgConnectionProxyNote": "Если у вас проблемы с проверкой подключения, проверьте конфигурацию обратного прокси.", - "obsidianLiveSyncSettingTab.msgCurrentOrigin": "Текущий origin: origin", + "obsidianLiveSyncSettingTab.msgCurrentOrigin": "Текущий origin: ${origin}", "obsidianLiveSyncSettingTab.msgDiscardConfirmation": "Вы действительно хотите отменить существующие настройки и базы данных?", "obsidianLiveSyncSettingTab.msgDone": "--Готово--", "obsidianLiveSyncSettingTab.msgEnableCors": "Установить httpd.enable_cors", @@ -439,15 +648,15 @@ "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": "Не удаётся подключиться к не-HTTPS URI. Обновите конфигурацию.", "obsidianLiveSyncSettingTab.msgNotice": "---Уведомление---", "obsidianLiveSyncSettingTab.msgObjectStorageWarning": "ПРЕДУПРЕЖДЕНИЕ: Эта функция в разработке.", - "obsidianLiveSyncSettingTab.msgOriginCheck": "Проверка origin: org", - "obsidianLiveSyncSettingTab.msgRebuildRequired": "Требуется перестроение баз данных для применения изменений.", + "obsidianLiveSyncSettingTab.msgOriginCheck": "Проверка origin: ${org}", + "obsidianLiveSyncSettingTab.msgRebuildRequired": "Требуется перестроение баз данных для применения изменений. Выберите способ применения изменений.\n\n
\nОбозначения\n\n| Символ | Значение |\n|: ------ :| ------- |\n| ⇔ | Актуально |\n| ⇄ | Синхронизация для выравнивания |\n| ⇐,⇒ | Передача с перезаписью |\n| ⇠,⇢ | Передача с перезаписью с другой стороны |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\nКоротко: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nПерестроить локальную и удалённую базы данных по файлам, которые уже есть на этом устройстве.\nДругие устройства будут заблокированы, им потребуется загрузка с удалённого.\n## ${OPTION_FETCH}\nКоротко: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nОчистить локальную базу данных и построить её заново по данным, загруженным из удалённой базы данных.\nЭтот вариант подходит и тогда, когда вы перестроили удалённую базу данных.\n## ${OPTION_ONLY_SETTING}\nСохранить только настройки. **Внимание: это может привести к повреждению данных**; обычно перестроение базы данных необходимо.", "obsidianLiveSyncSettingTab.msgSetCorsCredentials": "Установить cors.credentials", "obsidianLiveSyncSettingTab.msgSetCorsOrigins": "Установить cors.origins", "obsidianLiveSyncSettingTab.msgSetMaxDocSize": "Установить couchdb.max_document_size", "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": "Установить chttpd.max_http_request_size", "obsidianLiveSyncSettingTab.msgSetRequireValidUser": "Установить chttpd.require_valid_user = true", "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": "Установить chttpd_auth.require_valid_user = true", - "obsidianLiveSyncSettingTab.msgSettingModified": "Настройка setting была изменена с другого устройства.", + "obsidianLiveSyncSettingTab.msgSettingModified": "Настройка «${setting}» была изменена с другого устройства. Нажмите {HERE}, чтобы перезагрузить настройки. Нажмите в другом месте, чтобы игнорировать изменения.", "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": "Эти настройки нельзя изменить во время синхронизации.", "obsidianLiveSyncSettingTab.msgSetWwwAuth": "Установить httpd.WWW-Authenticate", "obsidianLiveSyncSettingTab.nameApplySettings": "Применить настройки", @@ -493,8 +702,9 @@ "obsidianLiveSyncSettingTab.panelPrivacyEncryption": "Конфиденциальность и шифрование", "obsidianLiveSyncSettingTab.panelRemoteConfiguration": "Удалённая конфигурация", "obsidianLiveSyncSettingTab.panelSetup": "Настройка", - "obsidianLiveSyncSettingTab.serverVersion": "Информация о сервере: info", + "obsidianLiveSyncSettingTab.serverVersion": "Информация о сервере: ${info}", "obsidianLiveSyncSettingTab.titleActiveRemoteServer": "Активный удалённый сервер", + "obsidianLiveSyncSettingTab.titleAdvancedSettings": "Дополнительные настройки", "obsidianLiveSyncSettingTab.titleAppearance": "Внешний вид", "obsidianLiveSyncSettingTab.titleConflictResolution": "Разрешение конфликтов", "obsidianLiveSyncSettingTab.titleCouchDB": "Сервер CouchDB", @@ -502,11 +712,16 @@ "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": "Шифрование не включено", "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": "Парольная фраза шифрования недействительна", "obsidianLiveSyncSettingTab.titleExtraFeatures": "Включить дополнительные и расширенные функции", + "obsidianLiveSyncSettingTab.titleExtraFeaturesGroup": "Дополнительные возможности", + "obsidianLiveSyncSettingTab.titleExtraMenus": "Дополнительные меню", "obsidianLiveSyncSettingTab.titleFetchConfig": "Загрузить конфигурацию", "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": "Загрузить конфигурацию с удалённого сервера", "obsidianLiveSyncSettingTab.titleFetchSettings": "Загрузить настройки", + "obsidianLiveSyncSettingTab.titleHelpAndInformation": "Справка и сведения", + "obsidianLiveSyncSettingTab.titleHelpAndTroubleshooting": "Справка и решение проблем", "obsidianLiveSyncSettingTab.titleHiddenFiles": "Скрытые файлы", "obsidianLiveSyncSettingTab.titleLogging": "Логирование", + "obsidianLiveSyncSettingTab.titleMaintenanceAndRecovery": "Обслуживание и восстановление", "obsidianLiveSyncSettingTab.titleMinioS3R2": "MinIO, S3, R2", "obsidianLiveSyncSettingTab.titleNotification": "Уведомления", "obsidianLiveSyncSettingTab.titleOnlineTips": "Онлайн советы", @@ -516,23 +731,37 @@ "obsidianLiveSyncSettingTab.titleRemoteServer": "Удалённый сервер", "obsidianLiveSyncSettingTab.titleReset": "Сброс", "obsidianLiveSyncSettingTab.titleSetupOtherDevices": "Для настройки других устройств", + "obsidianLiveSyncSettingTab.titleSynchronisation": "Синхронизация", "obsidianLiveSyncSettingTab.titleSynchronizationMethod": "Метод синхронизации", "obsidianLiveSyncSettingTab.titleSynchronizationPreset": "Пресет синхронизации", "obsidianLiveSyncSettingTab.titleSyncSettings": "Настройки синхронизации", "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": "Синхронизация настроек через Markdown", "obsidianLiveSyncSettingTab.titleUpdateThinning": "Оптимизация обновлений", - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": "⚠ CORS Origin не совпадает from->to", + "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": "⚠ CORS Origin не совпадает ${from}->${to}", "obsidianLiveSyncSettingTab.warnNoAdmin": "⚠ У вас нет прав администратора.", + "Of course, we can back up the data before proceeding.": "Конечно, перед этим можно сделать резервную копию данных.", + "Off": "Выкл", "Ok": "ОК", "Old Algorithm": "Старый алгоритм", + "Older (${diff})": "Старше (${diff})", "Older fallback (Slow, W/O WebAssembly)": "Старый вариант fallback (медленный, без WebAssembly)", + "On": "Вкл", + "On the source device, from the command palette, run the 'Show settings as a QR code' command.": "На исходном устройстве в палитре команд выполните команду «Показать настройки как QR-код».", + "On the source device, open Obsidian.": "На исходном устройстве откройте Obsidian.", + "On this device, please keep this Vault open.": "На этом устройстве не закрывайте хранилище.", + "On this device, switch to the camera app or use a QR code scanner to scan the displayed QR code.": "На этом устройстве откройте камеру или сканер QR-кодов и считайте показанный QR-код.", "Open": "Открыть", + "Open connection": "Открыть соединение", + "Open P2P Setup...": "Открыть настройку P2P...", "Open the dialog": "Открыть диалог", + "Other files": "Другие файлы", "Overwrite": "Перезаписать", "Overwrite patterns": "Шаблоны перезаписи", "Overwrite remote": "Перезаписать удалённое хранилище", "Overwrite remote with local DB and passphrase.": "Перезаписать удалённое хранилище локальной БД и парольной фразой.", "Overwrite Server Data with This Device's Files": "Перезаписать данные сервера файлами с этого устройства", + "P2P Configuration": "Настройки P2P", + "P2P Status": "Состояние P2P", "P2P.AskPassphraseForDecrypt": "Удалённое устройство предоставило конфигурацию. Введите пароль для расшифровки.", "P2P.AskPassphraseForShare": "Удалённое устройство запрашивает эту конфигурацию. Введите пароль для передачи.", "P2P.DisabledButNeed": "title_p2p_sync отключён. Вы действительно хотите включить?", @@ -547,44 +776,68 @@ "P2P.P2PReplication": "P2P Репликация", "P2P.PaneTitle": "long_p2p_sync", "P2P.ReplicatorInstanceMissing": "P2P Sync репликатор не найден, возможно, не настроен.", - "P2P.SeemsOffline": "Устройство name офлайн, пропущено.", + "P2P.SeemsOffline": "Устройство ${name} офлайн, пропущено.", "P2P.SyncAlreadyRunning": "P2P Sync уже запущен.", "P2P.SyncCompleted": "P2P Sync завершён.", - "P2P.SyncStartedWith": "P2P Sync с name начат.", + "P2P.SyncStartedWith": "P2P Sync с ${name} начат.", "paneMaintenance.markDeviceResolvedAfterBackup": "Пометить устройство как обработанное после резервного копирования", "paneMaintenance.remoteLockedAndDeviceNotAccepted": "Удалённая база данных заблокирована, и это устройство ещё не одобрено.", "paneMaintenance.remoteLockedResolvedDevice": "Удалённая база данных заблокирована, но это устройство уже одобрено.", "paneMaintenance.unlockDatabaseReady": "Разблокировать базу данных", "Passphrase": "Парольная фраза", + "Passphrase is required.": "Нужна парольная фраза.", "Passphrase of sensitive configuration items": "Парольная фраза для конфиденциальных настроек", "password": "пароль", "Password": "Пароль", "Paste a connection string": "Вставить строку подключения", - "Paste the Setup URI generated from one of your active devices.": "Вставьте Setup URI, созданный на одном из ваших активных устройств。", + "Paste the Setup URI generated from one of your active devices.": "Вставьте Setup URI, созданный на одном из ваших активных устройств.", + "Path": "Путь", "Path Obfuscation": "Обфускация путей", "Patterns to match files for overwriting instead of merging": "Шаблоны для файлов, которые нужно перезаписывать вместо объединения", "Patterns to match files for syncing": "Шаблоны для файлов, которые нужно синхронизировать", + "Peer ID:": "Peer ID:", + "Peer to Peer Replicator": "P2P репликатор", "Peer-to-Peer only": "Только Peer-to-Peer", "Peer-to-Peer Synchronisation": "Одноранговая синхронизация", + "Peers": "Устройства", "Per-file-saved customization sync": "Синхронизация настроек для каждого файла", "Perform": "Выполнить", "Perform cleanup": "Выполнить очистку", "Perform Garbage Collection": "Выполнить сборку мусора", "Perform Garbage Collection to remove unused chunks and reduce database size.": "Выполняет сборку мусора, чтобы удалить неиспользуемые чанки и уменьшить размер базы данных.", "Periodic Sync interval": "Интервал периодической синхронизации", + "PERMANENT": "НАВСЕГДА", "Pick a file to resolve conflict": "Выбрать файл для разрешения конфликта", + "Pick a file to show history": "Выберите файл, чтобы посмотреть историю", + "Please be aware that the End-to-End Encryption passphrase is not validated until the synchronisation process actually commences. This is a security measure designed to protect your data.": "Учтите: парольная фраза сквозного шифрования проверяется только при запуске синхронизации. Так сделано для защиты данных.", + "Please configure your end-to-end encryption settings.": "Задайте настройки сквозного шифрования.", "Please disable 'Read chunks online' in settings to use Garbage Collection.": "Чтобы использовать Garbage Collection, отключите в настройках «Read chunks online».", "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": "Чтобы использовать Garbage Collection, включите в настройках «Compute revisions for chunks».", + "Please enter the CouchDB server information below.": "Укажите данные сервера CouchDB ниже.", + "Please enter the details required to connect to your S3/MinIO/R2 compatible object storage service.": "Укажите данные для подключения к объектному хранилищу, совместимому с S3/MinIO/R2.", + "Please enter the Peer-to-Peer Synchronisation information below.": "Укажите данные P2P-синхронизации ниже.", + "Please enter the Setup URI that was generated during server installation or on another device, along with the vault passphrase.": "Введите Setup URI, полученный при установке сервера или на другом устройстве, и парольную фразу Vault.", + "Please follow the steps below to import settings from your existing device.": "Выполните шаги ниже, чтобы перенести настройки с уже настроенного устройства.", + "PLEASE NOTE": "ОБРАТИТЕ ВНИМАНИЕ", "Please select 'Cancel' explicitly to cancel this operation.": "Чтобы отменить эту операцию, явно выберите «Отмена».", - "Please select a method to import the settings from another device.": "Выберите способ импорта настроек с другого устройства。", + "Please select a method to import the settings from another device.": "Выберите способ импорта настроек с другого устройства.", + "Please select an active P2P remote configuration to change P2P sync targets.": "Выберите активную настройку удалённой базы P2P, чтобы сменить цели P2P-синхронизации.", "Please select an option to proceed": "Чтобы продолжить, выберите вариант", - "Please select the type of server to which you are connecting.": "Выберите тип сервера, к которому вы подключаетесь。", + "Please select the button below to restart and proceed to the data fetching confirmation.": "Нажмите кнопку ниже, чтобы перезапустить и перейти к подтверждению загрузки данных.", + "Please select the button below to restart and proceed to the final confirmation.": "Нажмите кнопку ниже, чтобы перезапустить и перейти к последнему подтверждению.", + "Please select the type of server to which you are connecting.": "Выберите тип сервера, к которому вы подключаетесь.", + "Please select your situation.": "Выберите свой случай.", "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": "Укажите имя устройства для идентификации этого устройства. Имя должно быть уникальным среди ваших устройств. Пока оно не задано, мы не можем включить эту функцию.", "Please set this device name": "Укажите имя этого устройства", + "Please understand that this is intended behaviour.": "Так и задумано.", "Plug-in version": "Версия плагина", + "Plugins": "Плагины", "Prepare the 'report' to create an issue": "Подготовить «отчёт» для создания Issue", "Presets": "Пресеты", + "Prevent fetching configuration from server": "Не забирать настройки с сервера", + "Proceed": "Продолжить", "Proceed Garbage Collection": "Продолжить Garbage Collection", + "Proceed to the next step.": "Перейти к следующему шагу.", "Proceed with Setup URI": "Продолжить с Setup URI", "Proceeding with Garbage Collection, ignoring missing nodes.": "Продолжаем Garbage Collection, игнорируя отсутствующие узлы.", "Proceeding with Garbage Collection.": "Запускаем Garbage Collection.", @@ -596,6 +849,7 @@ "Purge all journal counter": "Очистить все счётчики журнала", "Rebuild local and remote database with local files.": "Перестроить локальную и удалённую базы данных на основе локальных файлов.", "Rebuilding Operations (Remote Only)": "Операции перестроения (только удалённое хранилище)", + "Recovery and Repair": "Восстановление и исправление", "Recreate all": "Пересоздать всё", "Recreate missing chunks for all files": "Пересоздать отсутствующие чанки для всех файлов", "RedFlag.Fetch.Method.Desc": "Как вы хотите загрузить?", @@ -607,16 +861,24 @@ "RedFlag.FetchRemoteConfig.Buttons.Fetch": "Да, загрузить и применить удалённые настройки", "RedFlag.FetchRemoteConfig.Message": "Вы хотите загрузить и применить удалённые настройки?", "RedFlag.FetchRemoteConfig.Title": "Загрузить удалённую конфигурацию", + "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.": "Освобождает место: удаляет все ревизии, кроме последней. Нужно столько же свободного места на сервере и на устройстве.", "Reducing the frequency with which on-disk changes are reflected into the DB": "Уменьшение частоты отражения изменений с диска в БД", + "Refresh": "Обновить", "Region": "Регион", + "Relay settings": "Настройки ретранслятора", + "Reload": "Перезагрузить", "Remediation": "Исправление", "Remediation Setting Changed": "Настройки исправления изменены", "Remote Database Tweak (In sunset)": "Настройки удалённой базы данных (устаревает)", "Remote Databases": "Удалённые базы данных", "Remote name": "Имя удалённого хранилища", + "Remote only": "Только в удалённой базе", "Remote server type": "Тип удалённого сервера", "Remote Type": "Удалённый тип", "Rename": "Переименовать", + "Replicate now": "Реплицировать сейчас", + "Replicating": "Идёт репликация", + "Replicating...": "Идёт репликация...", "Replicator.Dialogue.Locked.Action.Dismiss": "Отмена для подтверждения", "Replicator.Dialogue.Locked.Action.Fetch": "Сбросить синхронизацию на этом устройстве", "Replicator.Dialogue.Locked.Action.Unlock": "Разблокировать удалённую базу данных", @@ -639,6 +901,7 @@ "Reset": "Сброс", "Reset all": "Сбросить всё", "Reset all journal counter": "Сбросить все счётчики журнала", + "Reset and Resume Synchronisation": "Сбросить и продолжить синхронизацию", "Reset journal received history": "Сбросить историю полученных записей журнала", "Reset journal sent history": "Сбросить историю отправленных записей журнала", "Reset notification threshold and check the remote database usage": "Сбросить порог уведомления и проверить использование удалённой базы данных", @@ -651,32 +914,60 @@ "Resolve all conflicted files": "Разрешить все конфликтующие файлы", "Resolve All conflicted files by the newer one": "Разрешить все конфликтующие файлы в пользу более новой версии", "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": "Разрешает все конфликтующие файлы в пользу более новой версии. Внимание: старая версия будет перезаписана и её нельзя будет восстановить.", + "Restart and Fetch Data": "Перезапустить и забрать данные с сервера", + "Restart and Initialise Server": "Перезапустить и инициализировать сервер", "Restart Now": "Перезапустить сейчас", + "Restarting Obsidian is strongly recommended. Until restart, some changes may not take effect, and display may be inconsistent. Are you sure to restart now?": "Лучше перезапустить Obsidian. До перезапуска часть изменений не применится, а вид может быть неверным. Перезапустить сейчас?", "Restore or reconstruct local database from remote.": "Восстановить или перестроить локальную базу данных из удалённой.", + "Rev": "Ревизия", + "Revert changes": "Откатить изменения", + "Revoke": "Отозвать", + "Room ID": "ID комнаты", + "Room ID suffix:": "Суффикс ID комнаты:", "Run Doctor": "Запустить диагностику", + "S3/MinIO/R2 Configuration": "Настройки S3/MinIO/R2", "S3/MinIO/R2 Object Storage": "Объектное хранилище S3/MinIO/R2", + "Same": "Совпадает", + "Same or local only": "Совпадает или есть только локально", + "Save and Apply": "Сохранить и применить", "Save settings to a markdown file.": "Сохранить настройки в файл markdown.", - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.", + "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": "Сохранить настройки в файл markdown. Вы получите уведомление, когда придут новые настройки. Для разных платформ можно задать разные файлы.", + "Save without connecting": "Сохранить без проверки связи", "Saving will be performed forcefully after this number of seconds.": "Сохранение будет принудительно выполнено после этого количества секунд.", + "Saving without a successful connection test keeps this profile, but automatic synchronisation may fail until the connection is corrected.": "Профиль сохранится и без успешной проверки связи, но автоматическая синхронизация может не работать, пока подключение не исправлено.", "Scan a QR Code (Recommended for mobile)": "Сканировать QR-код (рекомендуется для мобильных устройств)", + "Scan changes": "Проверить изменения", "Scan changes on customization sync": "Сканировать изменения при синхронизации настроек", "Scan customization automatically": "Сканировать настройки автоматически", "Scan customization before replicating.": "Сканировать настройки перед репликацией.", "Scan customization every 1 minute.": "Сканировать настройки каждую минуту.", "Scan customization periodically": "Сканировать настройки периодически", + "Scan for Broken files": "Найти повреждённые файлы", "Scan for hidden files before replication": "Сканировать скрытые файлы перед репликацией", "Scan hidden files periodically": "Сканировать скрытые файлы периодически", - "Scan the QR code displayed on an active device using this device's camera.": "Отсканируйте QR-код, показанный на активном устройстве, с помощью камеры этого устройства。", + "Scan QR Code": "Сканировать QR-код", + "Scan the QR code displayed on an active device using this device's camera.": "Отсканируйте QR-код, показанный на активном устройстве, с помощью камеры этого устройства.", "Schedule and Restart": "Запланировать и перезапустить", + "Scram Switches": "Аварийные выключатели", "Scram!": "Экстренные меры", "Seconds, 0 to disable": "Секунд, 0 для отключения", "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": "Секунды. Сохранение в локальную базу данных будет отложено.", + "Secret Access Key": "Secret Access Key", "Secret Key": "Секретный ключ", + "Select active P2P remote": "Выберите активную удалённую базу P2P", + "Select All Shiny": "Выбрать все новые", + "Select Flagged Shiny": "Выбрать отмеченные новые", + "Select P2P remote...": "Выберите удалённую базу P2P...", "Select the database adapter to use.": "Выберите используемый адаптер базы данных.", + "Selecting this option will result in the current data on this device being used to initialise the server. Any existing data on the server will be completely overwritten.": "Сервер получит данные с этого устройства. Всё, что уже есть на сервере, будет полностью перезаписано.", + "Selecting this option will result in this device joining the existing server. You need to fetching the existing synchronisation data from the server to this device.": "Это устройство подключится к уже настроенному серверу. Данные синхронизации надо забрать с сервера на это устройство.", + "Selective": "Выборочно", "Send": "Отправить", "Send chunks": "Отправить чанки", + "SENDING": "ОТПРАВКА", "Server URI": "URI сервера", - "Setting.GenerateKeyPair.Desc": "Мы сгенерировали пару ключей!", + "SESSION": "СЕАНС", + "Setting.GenerateKeyPair.Desc": "Мы сгенерировали пару ключей!\n\nПримечание: эта пара ключей больше никогда не будет показана. Сохраните её в надёжном месте. Если вы её потеряете, вам нужно будет сгенерировать новую пару ключей.\nПримечание 2: открытый ключ в формате spki, а закрытый ключ — в формате pkcs8. Для удобства переносы строк в открытом ключе заменены на `\\n`.\nПримечание 3: открытый ключ настраивается в удалённой базе данных, а закрытый ключ — на локальных устройствах.\n\n>[!FOR YOUR EYES ONLY]-\n>
\n>\n> ### Открытый ключ\n> ```\n${public_key}\n> ```\n>\n> ### Закрытый ключ\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Both for copying]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
\n\n", "Setting.GenerateKeyPair.Title": "Новая пара ключей сгенерирована!", "Setting.TroubleShooting": "Устранение неполадок", "Setting.TroubleShooting.Doctor": "Диагностика настроек", @@ -684,14 +975,17 @@ "Setting.TroubleShooting.ScanBrokenFiles": "Сканировать повреждённые файлы", "Setting.TroubleShooting.ScanBrokenFiles.Desc": "Сканирует файлы, которые неправильно хранятся в базе данных.", "SettingTab.Message.AskRebuild": "Ваши изменения требуют загрузки из удалённой базы данных. Хотите продолжить?", + "Setup Complete: Preparing to Fetch Synchronisation Data": "Настройка завершена: готовимся забрать данные синхронизации", + "Setup Complete: Preparing to Initialise Server": "Настройка завершена: готовимся инициализировать сервер", "Setup URI dialog cancelled.": "Диалог Setup URI был отменён.", + "Setup-URI": "Setup-URI", "Setup.Apply.Buttons.ApplyAndFetch": "Применить и загрузить", "Setup.Apply.Buttons.ApplyAndMerge": "Применить и объединить", "Setup.Apply.Buttons.ApplyAndRebuild": "Применить и перестроить", "Setup.Apply.Buttons.Cancel": "Отменить и отменить", "Setup.Apply.Buttons.OnlyApply": "Только применить", "Setup.Apply.Message": "Новая конфигурация готова. Есть несколько способов применить её.", - "Setup.Apply.Title": "Применить новую конфигурацию из method", + "Setup.Apply.Title": "Применить новую конфигурацию из ${method}", "Setup.Apply.WarningRebuildRecommended": "ПРИМЕЧАНИЕ: после настройки изменений определено, что требуется перестроение.", "Setup.Doctor.Buttons.No": "Нет, использовать настройки из URI как есть", "Setup.Doctor.Buttons.Yes": "Да, пожалуйста, запустить диагностику", @@ -701,7 +995,7 @@ "Setup.FetchRemoteConf.Buttons.Skip": "Нет, использовать настройки из URI", "Setup.FetchRemoteConf.Message": "Если мы уже синхронизировались с другим устройством, удалённая база данных хранит подходящие значения конфигурации.", "Setup.FetchRemoteConf.Title": "Загрузить конфигурацию с удалённой базы данных?", - "Setup.QRCode": "Мы сгенерировали QR-код для передачи настроек. Отсканируйте QR-код телефоном.", + "Setup.QRCode": "Мы сгенерировали QR-код для передачи настроек. Отсканируйте QR-код телефоном или другим устройством.\nВнимание: QR-код не зашифрован, открывайте его осторожно.\n\n>[!FOR YOUR EYES ONLY]-\n>
${qr_image}
", "Setup.RemoteE2EE.AdvancedTitle": "Дополнительно", "Setup.RemoteE2EE.AlgorithmWarning": "Изменение алгоритма шифрования лишит доступа к данным, которые ранее были зашифрованы другим алгоритмом. Убедитесь, что все ваши устройства настроены на использование одного и того же алгоритма, чтобы сохранить доступ к данным.", "Setup.RemoteE2EE.ButtonCancel": "Отмена", @@ -746,21 +1040,38 @@ "Should we prompt you about conflicting files when a file is opened?": "Спрашивать о конфликтующих файлах при открытии файла?", "Should we prompt you for every single merge, even if we can safely merge automatcially?": "Спрашивать о каждом слиянии, даже если мы можем безопасно слить автоматически?", "Show full banner": "Показывать полный баннер", + "Show history": "Показать историю", + "Show icon only": "Показывать только значок", "Show only notifications": "Показывать только уведомления", + "Show password": "Показать пароль", "Show status as icons only": "Показывать статус только иконками", "Show status icon instead of file warnings banner": "Показывать иконку статуса вместо предупреждения о файлах", "Show status inside the editor": "Показывать статус внутри редактора", "Show status on the status bar": "Показывать статус в строке состояния", "Show verbose log. Please enable if you report an issue.": "Показывать подробный лог. Пожалуйста, включите при сообщении о проблеме.", + "Signaling Server Connection": "Подключение к сигнальному серверу", + "Signalling Status": "Состояние сигнального соединения", + "Skip and close": "Пропустить и закрыть", + "Snippets": "Сниппеты", "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": "У некоторых устройств различаются значения прогресса (макс.: ${maxProgress}, мин.: ${minProgress}).\nЭто может означать, что некоторые устройства ещё не завершили синхронизацию, что может привести к конфликтам. Настоятельно рекомендуется перед продолжением убедиться, что все устройства синхронизированы.", + "Start Broadcasting": "Начать вещание", + "Start change-broadcasting on Connect": "Начинать вещание изменений при подключении", + "Start Sync & Close": "Запустить синхронизацию и закрыть", "Starts synchronisation when a file is saved.": "Запускать синхронизацию при сохранении файла.", + "Stat": "Статистика", + "Stats": "Статистика", + "Stop ⚡": "Стоп ⚡", + "Stop Broadcasting": "Остановить вещание", "Stop reflecting database changes to storage files.": "Остановить отражение изменений базы данных в файлы хранилища.", "Stop watching for file changes.": "Остановить отслеживание изменений файлов.", + "Storage -> Database": "Файлы -> База данных", + "Strongly Recommended": "Настоятельно рекомендуем", "Suppress notification of hidden files change": "Подавлять уведомления об изменении скрытых файлов", "Suspend database reflecting": "Приостановить отражение базы данных", "Suspend file watching": "Приостановить отслеживание файлов", "Switch to IDB": "Переключиться на IDB", "Switch to IndexedDB": "Переключиться на IndexedDB", + "Sync": "Синхронизация", "Sync after merging file": "Синхронизировать после слияния файла", "Sync automatically after merging files": "Синхронизировать автоматически после слияния файлов", "Sync Mode": "Режим синхронизации", @@ -768,29 +1079,67 @@ "Sync on File Open": "Синхронизация при открытии файла", "Sync on Save": "Синхронизация при сохранении", "Sync on Startup": "Синхронизация при запуске", - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": "Синхронизация с использованием файлов журнала. Необходимо заранее настроить объектное хранилище, совместимое с S3/MinIO/R2。", + "Sync once": "Синхронизировать один раз", + "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": "Синхронизация с использованием файлов журнала. Необходимо заранее настроить объектное хранилище, совместимое с S3/MinIO/R2.", "Synchronising files": "Синхронизируемые файлы", "Syncing": "Синхронизация", + "Syncing...": "Синхронизация...", "Target patterns": "Целевые шаблоны", + "Test connection and save": "Проверить подключение и сохранить", + "Test Settings and Continue": "Проверить настройки и продолжить", "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": "Только для тестирования - разрешать конфликты файлов синхронизацией новых копий.", + "The connection test cannot add a signalling relay while P2P is active. Use the active relay settings, or disconnect P2P before testing.": "Пока P2P активен, проверка связи не может добавить сигнальный ретранслятор. Используйте текущие настройки ретранслятора или отключите P2P перед проверкой.", + "The connection to the server has been configured successfully. As the next step,": "Подключение к серверу настроено. Следующим шагом", "The delay for consecutive on-demand fetches": "Задержка для последовательных запросов по требованию", + "The files in this Vault are almost identical to the server's.": "Файлы в этом хранилище почти совпадают с файлами на сервере.", "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": "Для следующих принятых узлов отсутствует информация об узле:\n- ${missingNodes}\n\nЭто означает, что они давно не подключались или остались на старой версии.\nПо возможности рекомендуется сначала обновить все устройства. Если у вас есть устройства, которые больше не используются, вы можете очистить список всех принятых узлов, один раз заблокировав удалённую базу.", + "The Group ID and passphrase are used to identify your group of devices. Make sure to use the same Group ID and passphrase on all devices you want to synchronise.": "ID группы и парольная фраза определяют вашу группу устройств. Задайте одинаковые ID группы и парольную фразу на всех устройствах, которые хотите синхронизировать.", "The Hash algorithm for chunk IDs": "Хэш-алгоритм для ID чанков", + "The IndexedDB adapter often offers superior performance in certain scenarios, but it has been found to cause memory leaks when used with LiveSync mode. When using LiveSync mode, please use IDB adapter instead.": "Адаптер IndexedDB в некоторых случаях работает заметно быстрее, но в режиме LiveSync он вызывает утечки памяти. В режиме LiveSync используйте адаптер IDB.", + "the latest synchronisation data will be downloaded from the server to this device.": "на это устройство будут загружены свежие данные синхронизации с сервера.", + "the local database, that is to say the synchronisation information, must be reconstituted.": "локальную базу, то есть данные синхронизации, надо собрать заново.", "The maximum duration for which chunks can be incubated within the document.": "Максимальная продолжительность инкубации чанков в документе.", - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.", + "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": "Максимальная продолжительность инкубации чанков в документе. Чанки, превысившие этот срок, перейдут в независимые чанки.", "The maximum number of chunks that can be incubated within the document.": "Максимальное количество инкубируемых чанков в документе.", - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.", + "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": "Максимальное количество инкубируемых чанков в документе. Чанки сверх этого количества сразу перейдут в независимые чанки.", "The maximum total size of chunks that can be incubated within the document.": "Максимальный общий размер инкубируемых чанков в документе.", - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.", + "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": "Максимальный общий размер инкубируемых чанков в документе. Чанки, превысившие этот размер, сразу перейдут в независимые чанки.", "The minimum interval for automatic synchronisation on event.": "Минимальный интервал автоматической синхронизации по событию.", - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": "Эта функция обеспечивает прямую синхронизацию между устройствами. Сервер не требуется, но для синхронизации оба устройства должны быть одновременно в сети, а некоторые функции могут быть ограничены. Подключение к Интернету нужно только для сигнализации (обнаружения пиров), а не для передачи данных。", - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": "Это расширенный вариант для пользователей, у которых нет URI или которые хотят вручную задать подробные параметры。", - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": "Это наиболее подходящий для данной архитектуры способ синхронизации. Доступны все функции. Необходимо заранее развернуть экземпляр CouchDB。", + "The remote is already set up, and the configuration is compatible (or got compatible by this operation).": "Удалённая база уже настроена, и настройки совместимы (или стали совместимы после этой операции).", + "The Setup-URI does not appear to be valid. Please check that you have copied it correctly.": "Setup-URI выглядит неверным. Проверьте, что скопировали его правильно.", + "The Setup-URI is valid and ready to use.": "Setup-URI верный и готов к работе.", + "the single, authoritative master copy": "единственный достоверный источник", + "the synchronisation data on the server will be built based on the current data on this device.": "данные синхронизации на сервере будут построены по текущим данным этого устройства.", + "Themes": "Темы", + "There is a way to resolve this on other devices.": "На других устройствах это можно решить.", + "There may be differences between the files in this Vault and the server.": "Файлы в этом хранилище могут отличаться от файлов на сервере.", + "Therefore, we ask that you exercise extreme caution when configuring server information manually. If an incorrect passphrase is entered, the data on the server will become corrupted.": "Поэтому вводите данные сервера вручную очень внимательно. Если ввести неверную парольную фразу, данные на сервере испортятся.", + "This can isolate your connections between devices. Use the same Room ID for the same devices.": "Так можно разделить подключения между устройствами. Для одних и тех же устройств задайте одинаковый ID комнаты.", + "This device": "Это устройство", + "This device name": "Имя этого устройства", + "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": "Эта функция обеспечивает прямую синхронизацию между устройствами. Сервер не требуется, но для синхронизации оба устройства должны быть одновременно в сети, а некоторые функции могут быть ограничены. Подключение к Интернету нужно только для сигнализации (обнаружения пиров), а не для передачи данных.", + "This first setup has several short steps because it confirms encryption, the connection method, and which device provides the initial data. Once it is complete, additional devices can reuse a Setup URI.": "Первая настройка идёт в несколько коротких шагов: надо подтвердить шифрование, способ подключения и то, какое устройство даёт первые данные. После этого другие устройства можно настроить через Setup URI.", + "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": "Это расширенный вариант для пользователей, у которых нет URI или которые хотят вручную задать подробные параметры.", + "This is an extremely powerful operation. We strongly recommend that you copy your Vault folder to a safe location.": "Это очень мощная операция. Настоятельно рекомендуем скопировать папку хранилища в безопасное место.", + "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": "Это наиболее подходящий для данной архитектуры способ синхронизации. Доступны все функции. Необходимо заранее развернуть экземпляр CouchDB.", + "This optional check uses Obsidian's internal request API and sends the credentials above to the CouchDB server. Use it only with a server you trust; administrator access may be required.": "Эта необязательная проверка использует внутренний API запросов Obsidian и отправляет указанные выше учётные данные на сервер CouchDB. Пользуйтесь ей только с доверенным сервером; могут понадобиться права администратора.", "This passphrase will not be copied to another device. It will be set to until you configure it again.": "Эта парольная фраза не будет скопирована на другое устройство.", - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.", + "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": "Эта парольная фраза не будет скопирована на другое устройство. Ей будет присвоено значение `Default`, пока вы не настроите её снова.", + "This password is used to encrypt the connection. Use something long enough.": "Этот пароль шифрует подключение. Возьмите достаточно длинный.", + "This procedure will first delete all existing synchronisation data from the server. Following this, the server data will be completely rebuilt, using the current state of your Vault on this device (including its local database) as": "Сначала все данные синхронизации на сервере будут удалены. Затем данные на сервере соберутся заново, а за основу возьмётся текущее состояние хранилища на этом устройстве (вместе с локальной базой) как", + "This setting must be the same even when connecting to multiple synchronisation destinations.": "Этот параметр должен совпадать даже при подключении к нескольким целям синхронизации.", + "This Vault is empty, or contains only new files that are not on the server.": "Это хранилище пустое или содержит только новые файлы, которых нет на сервере.", + "This will rebuild the local database on this device using the most recent data from the server. This action is designed to resolve synchronisation inconsistencies and restore correct functionality.": "Локальная база на этом устройстве будет пересобрана по самым свежим данным с сервера. Так решаются расхождения синхронизации и восстанавливается правильная работа.", "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": "Это пересоздаст чанки для всех файлов. Если какие-то чанки отсутствовали, это может исправить ошибки.", + "To minimise the creation of new conflicts": "Чтобы создавалось меньше новых конфликтов", "Transfer Tweak": "Настройки передачи", + "TURN Credential": "Пароль TURN", + "TURN server settings are only necessary if you are behind a strict NAT or firewall that prevents direct P2P connections. In most cases, you can leave these fields blank.": "Настройки сервера TURN нужны только за строгим NAT или межсетевым экраном, который не даёт устройствам соединяться напрямую. Обычно эти поля можно оставить пустыми.", + "TURN Server URLs (comma-separated)": "Адреса серверов TURN (через запятую)", + "TURN Username": "Имя пользователя TURN", + "TweakMismatchResolve.Action.DisableAutoAcceptCompatible": "Отключить авто-приём", "TweakMismatchResolve.Action.Dismiss": "Отмена", + "TweakMismatchResolve.Action.EnableAutoAcceptCompatible": "Включить авто-приём", "TweakMismatchResolve.Action.UseConfigured": "Использовать настроенные параметры", "TweakMismatchResolve.Action.UseMine": "Обновить настройки удалённой базы данных", "TweakMismatchResolve.Action.UseMineAcceptIncompatible": "Обновить настройки, но оставить как есть", @@ -798,36 +1147,345 @@ "TweakMismatchResolve.Action.UseRemote": "Применить настройки к этому устройству", "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": "Применить настройки, но игнорировать несовместимость", "TweakMismatchResolve.Action.UseRemoteWithRebuild": "Применить настройки и загрузить снова", - "TweakMismatchResolve.Message.Main": "Настройки в удалённой базе данных следующие. Эти значения настроены другими устройствами.", - "TweakMismatchResolve.Message.MainTweakResolving": "Ваша конфигурация не совпадает с удалённым сервером.", + "TweakMismatchResolve.Message.AutoAcceptCompatibleUndefined": "\nПохоже, настройки на устройствах различаются. Совместимые изменения можно применять автоматически.\nВключить параметр `auto-accept`?", + "TweakMismatchResolve.Message.Main": "\nНастройки в удалённой базе данных следующие. Эти значения настроены другими устройствами, которые хотя бы раз синхронизировались с этим устройством.\n\nЕсли вы хотите использовать эти настройки, выберите %{TweakMismatchResolve.Action.UseConfigured}.\nЕсли вы хотите сохранить настройки этого устройства, выберите %{TweakMismatchResolve.Action.Dismiss}.\n\n${table}\n\n>[!TIP]\n> Если вы хотите синхронизировать все настройки, используйте «Синхронизация настроек через Markdown» после применения минимальной конфигурации этой функцией.\n\n${additionalMessage}", + "TweakMismatchResolve.Message.MainTweakResolving": "Ваша конфигурация не совпадает с конфигурацией на удалённом сервере.\n\nСледующие параметры должны совпадать:\n\n${table}\n\nСообщите нам своё решение.\n\n${additionalMessage}", + "TweakMismatchResolve.Message.mineUpdated": "Настройки устройства обновлены.", + "TweakMismatchResolve.Message.remoteUpdated": "Настройки на сервере обновлены.", "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": "Некоторые изменения совместимы, но могут потребовать дополнительного хранилища. Рекомендуется перестроение.", "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": "Некоторые удалённые конфигурации несовместимы с локальной базой данных. Требуется перестроение.", "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": "Обнаружены значения, несовместимые с удалённой базой данных. Рекомендуется перестроение.", "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": "Обнаружены значения, несовместимые с удалённой базой данных. Требуется перестроение.", - "TweakMismatchResolve.Table": "| Имя значения | Это устройство | На удалённом |\n|: --- |: ---- :|: ---- :|", - "TweakMismatchResolve.Table.Row": "| name | self | remote |", + "TweakMismatchResolve.Table": "| Имя значения | Это устройство | На удалённом |\n|: --- |: ---- :|: ---- :|\n${rows}\n\n", + "TweakMismatchResolve.Table.Row": "| ${name} | ${self} | ${remote} |", "TweakMismatchResolve.Title": "Обнаружено несоответствие конфигурации", + "TweakMismatchResolve.Title.AutoAcceptCompatible": "Доступен авто-приём", "TweakMismatchResolve.Title.TweakResolving": "Обнаружено несоответствие конфигурации", "TweakMismatchResolve.Title.UseRemoteConfig": "Использовать удалённую конфигурацию", + "Ui.Common.LocalDatabaseInitialisationFailed": "Self-hosted LiveSync не может синхронизировать. Соберите отчёт, чтобы посмотреть подробный журнал.", + "Ui.Common.Signal.Caution": "ОСТОРОЖНО", + "Ui.Common.Signal.Danger": "ОПАСНО", + "Ui.Common.Signal.Notice": "К СВЕДЕНИЮ", + "Ui.Common.Signal.Warning": "ВНИМАНИЕ", + "Ui.Common.SomeFilesCouldNotBeSynchronised": "Часть файлов синхронизировать не удалось. Проверьте эти файлы. Соберите отчёт, чтобы посмотреть подробный журнал.", + "Ui.Settings.Advanced.LocalDatabaseTweak": "Тонкая настройка локальной базы", + "Ui.Settings.Advanced.MemoryCache": "Кеш в памяти", + "Ui.Settings.Advanced.TransferTweak": "Тонкая настройка передачи", + "Ui.Settings.Common.Analyse": "Анализировать", + "Ui.Settings.Common.Back": "Назад", + "Ui.Settings.Common.Check": "Проверить", + "Ui.Settings.Common.Configure": "Настроить", + "Ui.Settings.Common.Continue": "Продолжить", + "Ui.Settings.Common.Delete": "Удалить", + "Ui.Settings.Common.Fetch": "Забрать с сервера", + "Ui.Settings.Common.Lock": "Заблокировать", + "Ui.Settings.Common.Merge": "Объединить", + "Ui.Settings.Common.Open": "Открыть", + "Ui.Settings.Common.Overwrite": "Перезаписать", + "Ui.Settings.Common.Perform": "Выполнить", + "Ui.Settings.Common.ResetAll": "Сбросить всё", + "Ui.Settings.Common.ResolveAll": "Разрешить всё", + "Ui.Settings.Common.Scan": "Сканировать", + "Ui.Settings.Common.Send": "Отправить", + "Ui.Settings.Common.Use": "Использовать", + "Ui.Settings.Common.VerifyAll": "Проверить все", + "Ui.Settings.CustomizationSync.OpenDesc": "Открыть диалог", + "Ui.Settings.CustomizationSync.Panel": "Синхронизация настроек", + "Ui.Settings.CustomizationSync.WarnChangeDeviceName": "Пока эта функция включена, имя устройства сменить нельзя. Отключите её, чтобы сменить имя.", + "Ui.Settings.CustomizationSync.WarnSetDeviceName": "Задайте имя устройства, чтобы отличать его от других. Имя должно быть уникальным среди ваших устройств. Без имени функцию включить нельзя.", + "Ui.Settings.Hatch.AnalyseDatabaseUsage": "Проанализировать занятое место в базе", + "Ui.Settings.Hatch.AnalyseDatabaseUsageDesc": "Разбирает, чем занята база, и делает TSV-отчёт для самостоятельной диагностики. Отчёт можно вставить в любую таблицу.", + "Ui.Settings.Hatch.BackToNonConfigured": "Вернуться к ненастроенному состоянию", + "Ui.Settings.Hatch.ConvertNonObfuscated": "Найти и преобразовать файлы без обфускации путей", + "Ui.Settings.Hatch.ConvertNonObfuscatedDesc": "Проверяет локальную базу на файлы, сохранённые без обфускации путей, и при необходимости преобразует их.", + "Ui.Settings.Hatch.CopyIssueReport": "Скопировать отчёт в буфер обмена", + "Ui.Settings.Hatch.DatabaseLabel": "База: ${details}", + "Ui.Settings.Hatch.DatabaseToStorage": "База данных -> Файлы", + "Ui.Settings.Hatch.DeleteCustomizationSyncData": "Удалить все данные синхронизации настроек", + "Ui.Settings.Hatch.GeneratedReport": "Готовый отчёт", + "Ui.Settings.Hatch.Missing": "Отсутствует", + "Ui.Settings.Hatch.ModifiedSize": "Изменён: ${modified}, размер: ${size}", + "Ui.Settings.Hatch.ModifiedSizeActual": "Изменён: ${modified}, размер: ${size} (фактический размер: ${actualSize})", + "Ui.Settings.Hatch.PrepareIssueReport": "Собрать «отчёт» для заведения issue", + "Ui.Settings.Hatch.RecoveryAndRepair": "Восстановление и исправление", + "Ui.Settings.Hatch.RecreateAll": "Пересоздать все", + "Ui.Settings.Hatch.RecreateMissingChunks": "Пересоздать недостающие чанки для всех файлов", + "Ui.Settings.Hatch.RecreateMissingChunksDesc": "Пересоздаёт чанки для всех файлов. Если каких-то чанков не хватало, это может исправить ошибки.", + "Ui.Settings.Hatch.ResetPanel": "Сброс", + "Ui.Settings.Hatch.ResetRemoteUsage": "Сбросить порог уведомления и проверить занятое место в удалённой базе", + "Ui.Settings.Hatch.ResetRemoteUsageDesc": "Сбрасывает порог размера удалённого хранилища и снова проверяет его размер.", + "Ui.Settings.Hatch.ResolveAllConflictedFiles": "Разрешить все конфликтующие файлы в пользу более новой версии", + "Ui.Settings.Hatch.ResolveAllConflictedFilesDesc": "Разрешает все конфликтующие файлы в пользу более новой версии. Внимание: старая версия будет перезаписана, вернуть её нельзя.", + "Ui.Settings.Hatch.RunDoctor": "Запустить диагностику", + "Ui.Settings.Hatch.ScanBrokenFiles": "Найти повреждённые файлы", + "Ui.Settings.Hatch.ScramSwitches": "Аварийные выключатели", + "Ui.Settings.Hatch.ShowHistory": "Показать историю", + "Ui.Settings.Hatch.StorageLabel": "Файлы: ${details}", + "Ui.Settings.Hatch.StorageToDatabase": "Файлы -> База данных", + "Ui.Settings.Hatch.VerifyAndRepairAllFiles": "Проверить и восстановить все файлы", + "Ui.Settings.Hatch.VerifyAndRepairAllFilesDesc": "Сравнивает содержимое файлов в локальной базе и на диске. Если они не совпадают, плагин спросит, какой вариант оставить.", + "Ui.Settings.Maintenance.Cleanup": "Выполнить очистку", + "Ui.Settings.Maintenance.CleanupDesc": "Освобождает место: удаляет все ревизии, кроме последней. Нужно столько же свободного места на сервере и на устройстве.", + "Ui.Settings.Maintenance.DeleteLocalDatabase": "Удалить локальную базу, чтобы сбросить или удалить Self-hosted LiveSync", + "Ui.Settings.Maintenance.EmergencyRestart": "Аварийный перезапуск", + "Ui.Settings.Maintenance.EmergencyRestartDesc": "Отключает всю синхронизацию и перезапускает плагин.", + "Ui.Settings.Maintenance.FreshStartWipe": "Стереть всё и начать заново", + "Ui.Settings.Maintenance.FreshStartWipeDesc": "Удаляет все данные на сервере.", + "Ui.Settings.Maintenance.GarbageCollection": "Garbage Collection V3 (бета)", + "Ui.Settings.Maintenance.GarbageCollectionAction": "Выполнить Garbage Collection", + "Ui.Settings.Maintenance.GarbageCollectionDesc": "Garbage Collection убирает неиспользуемые чанки и уменьшает размер базы.", + "Ui.Settings.Maintenance.LockServer": "Заблокировать сервер", + "Ui.Settings.Maintenance.LockServerDesc": "Блокирует сервер, чтобы другие устройства не синхронизировались.", + "Ui.Settings.Maintenance.OverwriteRemote": "Перезаписать удалённую базу", + "Ui.Settings.Maintenance.OverwriteRemoteDesc": "Перезаписывает удалённую базу локальной базой и парольной фразой.", + "Ui.Settings.Maintenance.OverwriteServerData": "Перезаписать данные на сервере файлами этого устройства", + "Ui.Settings.Maintenance.OverwriteServerDataDesc": "Пересобирает локальную и удалённую базу из файлов этого устройства.", + "Ui.Settings.Maintenance.PurgeAllJournalCounter": "Очистить все счётчики журнала", + "Ui.Settings.Maintenance.PurgeAllJournalCounterDesc": "Очищает все кеши загрузки и отправки.", + "Ui.Settings.Maintenance.RebuildingOperations": "Операции пересборки (только удалённая база)", + "Ui.Settings.Maintenance.Resend": "Отправить заново", + "Ui.Settings.Maintenance.ResendDesc": "Отправляет все чанки в удалённую базу заново.", + "Ui.Settings.Maintenance.Reset": "Сброс", + "Ui.Settings.Maintenance.ResetAllJournalCounter": "Сбросить все счётчики журнала", + "Ui.Settings.Maintenance.ResetAllJournalCounterDesc": "Обнуляет всю историю журнала. При следующей синхронизации каждый элемент будет получен и отправлен заново.", + "Ui.Settings.Maintenance.ResetJournalReceived": "Сбросить историю полученных записей журнала", + "Ui.Settings.Maintenance.ResetJournalReceivedDesc": "Обнуляет историю полученных записей журнала. При следующей синхронизации всё, кроме отправленного этим устройством, будет загружено заново.", + "Ui.Settings.Maintenance.ResetJournalSent": "Сбросить историю отправленных записей журнала", + "Ui.Settings.Maintenance.ResetJournalSentDesc": "Обнуляет историю отправленных записей журнала. При следующей синхронизации всё, кроме полученного этим устройством, будет отправлено заново.", + "Ui.Settings.Maintenance.ResetLocalSyncInfo": "Сбросить данные синхронизации", + "Ui.Settings.Maintenance.ResetLocalSyncInfoDesc": "Восстанавливает или пересобирает локальную базу из удалённой.", + "Ui.Settings.Maintenance.ResetReceived": "Сбросить полученное", + "Ui.Settings.Maintenance.ResetSentHistory": "Сбросить историю отправленного", + "Ui.Settings.Maintenance.ResetThisDevice": "Сбросить синхронизацию на этом устройстве", + "Ui.Settings.Maintenance.ScheduleAndRestart": "Запланировать и перезапустить", + "Ui.Settings.Maintenance.Scram": "Аварийная остановка!", + "Ui.Settings.Maintenance.SendChunks": "Отправить чанки", + "Ui.Settings.Maintenance.Syncing": "Синхронизация", + "Ui.Settings.Maintenance.WarningLockedReadyAction": "Я готов, разблокировать базу", + "Ui.Settings.Maintenance.WarningLockedReadyText": "Чтобы хранилище не испортилось, удалённая база заблокирована для синхронизации. (Это устройство помечено как «разрешённое».) Когда все ваши устройства будут помечены как «разрешённые», разблокируйте базу. Предупреждение будет появляться, пока репликация не подтвердит, что устройство разрешено.", + "Ui.Settings.Maintenance.WarningLockedResolveAction": "Я сделал резервную копию, отметить это устройство как разрешённое", + "Ui.Settings.Maintenance.WarningLockedResolveText": "Удалённая база заблокирована для синхронизации, чтобы хранилище не испортилось: это устройство не помечено как «разрешённое». Сделайте резервную копию хранилища, сбросьте локальную базу и выберите «Отметить это устройство как разрешённое». Предупреждение будет появляться, пока репликация не подтвердит, что устройство разрешено.", + "Ui.Settings.Maintenance.WriteRedFlagAndRestart": "Поставить флаг и перезапустить", + "Ui.Settings.Patches.CompatibilityConflict": "Совместимость (поведение при конфликтах)", + "Ui.Settings.Patches.CompatibilityDatabase": "Совместимость (структура базы)", + "Ui.Settings.Patches.CompatibilityInternalApi": "Совместимость (использование внутреннего API)", + "Ui.Settings.Patches.CompatibilityMetadata": "Совместимость (метаданные)", + "Ui.Settings.Patches.CompatibilityRemote": "Совместимость (удалённая база)", + "Ui.Settings.Patches.CompatibilityTrouble": "Совместимость (исправленные проблемы)", + "Ui.Settings.Patches.CurrentAdapter": "Текущий адаптер: ${adapter}", + "Ui.Settings.Patches.DatabaseAdapter": "Адаптер базы данных", + "Ui.Settings.Patches.DatabaseAdapterDesc": "Выберите адаптер базы данных.", + "Ui.Settings.Patches.EdgeCaseBehaviour": "Обработка крайних случаев (поведение)", + "Ui.Settings.Patches.EdgeCaseDatabase": "Обработка крайних случаев (база данных)", + "Ui.Settings.Patches.EdgeCaseProcessing": "Обработка крайних случаев (обработка)", + "Ui.Settings.Patches.IndexedDbWarning": "Адаптер IndexedDB в некоторых случаях работает заметно быстрее, но в режиме LiveSync он вызывает утечки памяти. В режиме LiveSync используйте адаптер IDB.", + "Ui.Settings.Patches.MigratingToIdb": "Переносим все данные в IDB...", + "Ui.Settings.Patches.MigratingToIndexedDb": "Переносим все данные в IndexedDB...", + "Ui.Settings.Patches.MigrationIdbCompleted": "Перенос в IDB завершён. Obsidian сразу перезапустится с новой настройкой.", + "Ui.Settings.Patches.MigrationIdbCompletedFollowUp": "Перенос в IDB завершён. Переключите адаптер и перезапустите Obsidian.", + "Ui.Settings.Patches.MigrationIndexedDbCompleted": "Перенос в IndexedDB завершён. Obsidian сразу перезапустится с новой настройкой.", + "Ui.Settings.Patches.MigrationIndexedDbCompletedFollowUp": "Перенос в IndexedDB завершён. Переключите адаптер и перезапустите Obsidian.", + "Ui.Settings.Patches.MigrationWarning": "Смена этого параметра требует переноса данных и перезапуска Obsidian, это может занять время. Сделайте резервную копию данных перед началом.", + "Ui.Settings.Patches.OperationToIdb": "в IDB", + "Ui.Settings.Patches.OperationToIndexedDb": "в IndexedDB", + "Ui.Settings.Patches.Remediation": "Исправление", + "Ui.Settings.Patches.RemediationChanged": "Настройка исправления изменена", + "Ui.Settings.Patches.RemediationNoLimit": "Ограничение не задано", + "Ui.Settings.Patches.RemediationRestarting": "Настройка исправления изменена. Перезапускаем Obsidian...", + "Ui.Settings.Patches.RemediationRestartLater": "Позже", + "Ui.Settings.Patches.RemediationRestartMessage": "Настоятельно советуем перезапустить Obsidian. До перезапуска часть изменений не действует, а окно может показывать неверные данные. Перезапустить сейчас?", + "Ui.Settings.Patches.RemediationRestartNow": "Перезапустить сейчас", + "Ui.Settings.Patches.RemediationSuffixChanged": "Суффикс изменён. Открываем базу заново...", + "Ui.Settings.Patches.RemediationWithValue": "Ограничение: ${date} (${timestamp})", + "Ui.Settings.Patches.RemoteDatabaseSunset": "Настройка удалённой базы (устаревает)", + "Ui.Settings.Patches.SwitchToIDB": "Переключить на IDB", + "Ui.Settings.Patches.SwitchToIndexedDb": "Переключить на IndexedDB", + "Ui.Settings.PowerUsers.ConfigurationEncryption": "Шифрование настроек", + "Ui.Settings.PowerUsers.ConnectionTweak": "Тонкая настройка соединения с CouchDB", + "Ui.Settings.PowerUsers.ConnectionTweakDesc": "Если при работе с IBM Cloudant вы упёрлись в предел размера запроса, уменьшите размер пакета и лимит пакета.", + "Ui.Settings.PowerUsers.Default": "По умолчанию", + "Ui.Settings.PowerUsers.Developer": "Разработчик", + "Ui.Settings.PowerUsers.EncryptSensitiveConfig": "Шифровать важные параметры", + "Ui.Settings.PowerUsers.PromptPassphraseEveryLaunch": "Запрашивать парольную фразу при каждом запуске", + "Ui.Settings.PowerUsers.UseCustomPassphrase": "Использовать свою парольную фразу", + "Ui.Settings.Remote.Activate": "Включить", + "Ui.Settings.Remote.ActiveSuffix": " (активна)", + "Ui.Settings.Remote.AddConnection": "Добавить соединение", + "Ui.Settings.Remote.AddRemoteDefaultName": "Новая удалённая база", + "Ui.Settings.Remote.ConfigureAndChangeRemote": "Настроить и сменить удалённую базу", + "Ui.Settings.Remote.ConfigureE2EE": "Настроить E2EE", + "Ui.Settings.Remote.ConfigureRemote": "Настроить удалённую базу", + "Ui.Settings.Remote.DeleteRemoteConfirm": "Удалить настройку удалённой базы «${name}»?", + "Ui.Settings.Remote.DeleteRemoteTitle": "Удаление настройки удалённой базы", + "Ui.Settings.Remote.DisplayName": "Отображаемое имя", + "Ui.Settings.Remote.DuplicateRemote": "Дублировать удалённую базу", + "Ui.Settings.Remote.DuplicateRemoteSuffix": "${name} (копия)", + "Ui.Settings.Remote.E2EEConfiguration": "Настройка E2EE", + "Ui.Settings.Remote.Export": "Экспорт", + "Ui.Settings.Remote.FetchRemoteSettings": "Забрать настройки с сервера", + "Ui.Settings.Remote.ImportConnection": "Импорт соединения", + "Ui.Settings.Remote.ImportConnectionPrompt": "Вставьте строку соединения", + "Ui.Settings.Remote.ImportedCouchDb": "Импортированная CouchDB", + "Ui.Settings.Remote.ImportedRemote": "Удалённая база", + "Ui.Settings.Remote.MoreActions": "Ещё действия", + "Ui.Settings.Remote.PeerToPeerPanel": "Синхронизация между устройствами (P2P)", + "Ui.Settings.Remote.RemoteConfigurationPrefix": "Настройка удалённой базы", + "Ui.Settings.Remote.RemoteDatabases": "Удалённые базы", + "Ui.Settings.Remote.RemoteName": "Имя удалённой базы", + "Ui.Settings.Remote.RemoteNameCouchDb": "CouchDB ${host}", + "Ui.Settings.Remote.RemoteNameP2P": "P2P ${room}", + "Ui.Settings.Remote.RemoteNameS3": "S3 ${bucket}", + "Ui.Settings.Remote.Rename": "Переименовать", + "Ui.Settings.Selector.AddDefaultPatterns": "Добавить шаблоны по умолчанию", + "Ui.Settings.Selector.CrossPlatform": "Кроссплатформенные", + "Ui.Settings.Selector.Default": "По умолчанию", + "Ui.Settings.Selector.HiddenFiles": "Скрытые файлы", + "Ui.Settings.Selector.IgnorePatterns": "Шаблоны исключений", + "Ui.Settings.Selector.NonSynchronisingFiles": "Несинхронизируемые файлы", + "Ui.Settings.Selector.NonSynchronisingFilesDesc": "(RegExp) Если задано, изменения локальных и удалённых файлов по этому шаблону пропускаются.", + "Ui.Settings.Selector.NormalFiles": "Обычные файлы", + "Ui.Settings.Selector.OverwritePatterns": "Шаблоны перезаписи", + "Ui.Settings.Selector.OverwritePatternsDesc": "Шаблоны файлов, которые перезаписываются, а не сливаются", + "Ui.Settings.Selector.SynchronisingFiles": "Синхронизируемые файлы", + "Ui.Settings.Selector.SynchronisingFilesDesc": "(RegExp) Пусто — синхронизируются все файлы. Задайте регулярное выражение, чтобы ограничить набор файлов.", + "Ui.Settings.Selector.TargetPatterns": "Целевые шаблоны", + "Ui.Settings.Selector.TargetPatternsDesc": "Шаблоны файлов для синхронизации", + "Ui.Settings.Setup.RerunWizardButton": "Запустить мастер заново", + "Ui.Settings.Setup.RerunWizardDesc": "Запустить мастер первичной настройки, чтобы снова настроить Self-hosted LiveSync.", + "Ui.Settings.Setup.RerunWizardName": "Повторный запуск мастера настройки", + "Ui.Settings.SyncSettings.Fetch": "Забрать с сервера", + "Ui.Settings.SyncSettings.Merge": "Объединить", + "Ui.Settings.SyncSettings.Overwrite": "Перезаписать", + "Ui.SetupWizard.ApplySettingsInitialisation.ApplyWithoutInitialisation": "Применить без инициализации", + "Ui.SetupWizard.ApplySettingsInitialisation.Back": "Выбрать другой способ применить эти настройки", + "Ui.SetupWizard.ApplySettingsInitialisation.BypassGuidance": "Если применить только настройки, устройство может стать несовместимым с текущими данными синхронизации. Выбирайте это, только если точно знаете, что пересборка не нужна.", + "Ui.SetupWizard.ApplySettingsInitialisation.BypassTitle": "Применить настройки без инициализации?", + "Ui.SetupWizard.ApplySettingsInitialisation.ContinueFetch": "Продолжить и забрать с сервера", + "Ui.SetupWizard.ApplySettingsInitialisation.FetchOption": "Сбросить синхронизацию на этом устройстве", + "Ui.SetupWizard.ApplySettingsInitialisation.FetchOptionDesc": "После перезапуска локальная база этого устройства пересобирается из текущих данных удалённой базы. Затем файлы хранилища сверяются с этими данными.", + "Ui.SetupWizard.ApplySettingsInitialisation.FetchOptionP2PDesc": "После перезапуска выберите устройство-источник в сети. Локальная база LiveSync этого устройства пересоберётся из него.", + "Ui.SetupWizard.ApplySettingsInitialisation.Guidance": "Эти изменения меняют способ чтения данных синхронизации. Примените их вместе с инициализацией после перезапуска.", + "Ui.SetupWizard.ApplySettingsInitialisation.KeepEditing": "Продолжить правку", + "Ui.SetupWizard.ApplySettingsInitialisation.ProceedFetch": "Перезапустить и забрать данные с сервера", + "Ui.SetupWizard.ApplySettingsInitialisation.ProceedFetchP2P": "Перезапустить и выбрать устройство-источник", + "Ui.SetupWizard.ApplySettingsInitialisation.ProceedRebuild": "Перезапустить и перезаписать данные на сервере", + "Ui.SetupWizard.ApplySettingsInitialisation.ProceedRebuildP2P": "Перезапустить и подготовить это устройство", + "Ui.SetupWizard.ApplySettingsInitialisation.Question": "Какие данные использовать после перезапуска?", + "Ui.SetupWizard.ApplySettingsInitialisation.RebuildOption": "Перезаписать данные на сервере файлами этого устройства", + "Ui.SetupWizard.ApplySettingsInitialisation.RebuildOptionDesc": "Пересобрать локальную и удалённую базы из текущих файлов хранилища. Другим устройствам после этого нужно сбросить локальную синхронизацию.", + "Ui.SetupWizard.ApplySettingsInitialisation.RebuildOptionP2P": "Подготовить это устройство из этого хранилища", + "Ui.SetupWizard.ApplySettingsInitialisation.RebuildOptionP2PDesc": "Пересобрать локальную базу LiveSync из текущих файлов хранилища. Другое устройство при этом не перезаписывается.", + "Ui.SetupWizard.ApplySettingsInitialisation.RemoteVerificationGuidance": "Не удалось проверить удалённую базу с текущими учётными данными и настройками шифрования. Если продолжить, получение данных после перезапуска может не сработать.", + "Ui.SetupWizard.ApplySettingsInitialisation.RemoteVerificationTitle": "Не удалось проверить данные в удалённой базе", + "Ui.SetupWizard.ApplySettingsInitialisation.Title": "Применить настройки и заново запустить синхронизацию", + "Ui.SetupWizard.Common.Back": "Нет, вернуться назад", + "Ui.SetupWizard.Common.Cancel": "Отмена", + "Ui.SetupWizard.Common.ProceedSelectOption": "Выберите вариант, чтобы продолжить", + "Ui.SetupWizard.Intro.ExistingOption": "Я добавляю устройство к готовой синхронизации", + "Ui.SetupWizard.Intro.ExistingOptionDesc": "Выберите это, если синхронизация уже работает на другом компьютере или телефоне. Так вы подключите это устройство к готовой связке.", + "Ui.SetupWizard.Intro.Guidance": "Сейчас несколько вопросов помогут упростить настройку синхронизации.", + "Ui.SetupWizard.Intro.NewOption": "Я настраиваю синхронизацию впервые", + "Ui.SetupWizard.Intro.NewOptionDesc": "Выберите это, если настраиваете это устройство как первое в синхронизации.", + "Ui.SetupWizard.Intro.ProceedExisting": "Да, хочу добавить это устройство к готовой синхронизации", + "Ui.SetupWizard.Intro.ProceedNew": "Да, хочу настроить новую синхронизацию", + "Ui.SetupWizard.Intro.Question": "Сначала выберите вариант, который лучше описывает вашу ситуацию.", + "Ui.SetupWizard.Intro.Title": "Добро пожаловать в Self-hosted LiveSync", + "Ui.SetupWizard.Invitation.Start": "Начать настройку", + "Ui.SetupWizard.OutroAskUserMode.CompatibleOption": "Удалённая база уже настроена, и настройки совместимы (или стали совместимыми после этой операции).", + "Ui.SetupWizard.OutroAskUserMode.CompatibleOptionDesc": "Если вы не уверены, этот вариант опасен. Он считает, что настройки сервера совместимы с этим устройством. Если это не так, данные могут пропасть. Убедитесь, что понимаете последствия.", + "Ui.SetupWizard.OutroAskUserMode.ExistingOption": "Мой сервер уже настроен. Хочу подключить к нему это устройство.", + "Ui.SetupWizard.OutroAskUserMode.ExistingOptionDesc": "С этим вариантом устройство подключается к готовому серверу. Данные синхронизации нужно забрать с сервера на это устройство.", + "Ui.SetupWizard.OutroAskUserMode.Guidance": "Соединение с сервером настроено. Дальше нужно пересобрать локальную базу, то есть данные синхронизации.", + "Ui.SetupWizard.OutroAskUserMode.NewOption": "Я настраиваю новый сервер впервые / хочу сбросить свой сервер.", + "Ui.SetupWizard.OutroAskUserMode.NewOptionDesc": "С этим вариантом сервер создаётся заново из текущих данных этого устройства. Всё, что было на сервере, полностью перезаписывается.", + "Ui.SetupWizard.OutroAskUserMode.ProceedApplySettings": "Применить настройки", + "Ui.SetupWizard.OutroAskUserMode.ProceedNext": "Перейти к следующему шагу.", + "Ui.SetupWizard.OutroAskUserMode.Question": "Выберите свою ситуацию.", + "Ui.SetupWizard.OutroAskUserMode.Title": "Почти готово: нужно ваше решение", + "Ui.SetupWizard.OutroNewP2PUser.GuidanceNotice": "В P2P нет центральной копии на сервере, которую можно перезаписать. Этот шаг готовит только это устройство; держите его в сети, пока другое устройство забирает первые данные.", + "Ui.SetupWizard.OutroNewP2PUser.GuidancePrimary": "Соединение между устройствами настроено. Дальше локальная база LiveSync соберётся из текущих файлов хранилища.", + "Ui.SetupWizard.OutroNewP2PUser.Important": "ОБРАТИТЕ ВНИМАНИЕ", + "Ui.SetupWizard.OutroNewP2PUser.Proceed": "Перезапустить и подготовить это устройство", + "Ui.SetupWizard.OutroNewP2PUser.Question": "Нажмите кнопку ниже, чтобы перезапустить и перейти к подтверждению локальной инициализации.", + "Ui.SetupWizard.OutroNewP2PUser.Title": "Настройка завершена: готовим это устройство к P2P", + "Ui.SetupWizard.OutroNewUser.GuidancePrimary": "Соединение с сервером настроено. Дальше данные синхронизации на сервере соберутся из текущих данных этого устройства.", + "Ui.SetupWizard.OutroNewUser.GuidanceWarning": "После перезапуска данные этого устройства уйдут на сервер как главная копия. Всё лишнее, что сейчас на сервере, будет полностью перезаписано.", + "Ui.SetupWizard.OutroNewUser.Important": "ВАЖНО", + "Ui.SetupWizard.OutroNewUser.Proceed": "Перезапустить и инициализировать сервер", + "Ui.SetupWizard.OutroNewUser.Question": "Нажмите кнопку ниже, чтобы перезапустить и перейти к последнему подтверждению.", + "Ui.SetupWizard.OutroNewUser.Title": "Настройка завершена: готовимся инициализировать сервер", + "Ui.SetupWizard.RebuildEverythingP2P.ConfirmLocalReset": "Я понимаю, что это сбрасывает только локальную базу синхронизации на этом устройстве.", + "Ui.SetupWizard.RebuildEverythingP2P.ConfirmLocalResetNote": "Она пересобирается из текущих файлов хранилища.", + "Ui.SetupWizard.RebuildEverythingP2P.ConfirmTitle": "⚠️ Подтвердите следующее", + "Ui.SetupWizard.RebuildEverythingP2P.Guidance": "Эта операция удалит локальную базу LiveSync на этом устройстве и соберёт её заново из текущих файлов хранилища. Данные на других устройствах не удаляются и не перезаписываются.", + "Ui.SetupWizard.RebuildEverythingP2P.Note": "Держите это устройство в сети после инициализации, чтобы другое устройство смогло забрать с него хранилище.", + "Ui.SetupWizard.RebuildEverythingP2P.Proceed": "Понимаю, подготовить это устройство", + "Ui.SetupWizard.RebuildEverythingP2P.Title": "Последнее подтверждение: подготовка устройства к P2P", + "Ui.SetupWizard.SelectExisting.Guidance": "Вы добавляете это устройство к готовой синхронизации.", + "Ui.SetupWizard.SelectExisting.ManualOption": "Настроить удалённую базу вручную", + "Ui.SetupWizard.SelectExisting.ManualOptionDesc": "Настроить ту же удалённую базу вручную, как на других устройствах. Только для опытных пользователей.", + "Ui.SetupWizard.SelectExisting.ProceedManual": "Продолжить с ручной настройкой", + "Ui.SetupWizard.SelectExisting.ProceedQr": "Отсканируйте камерой этого устройства QR-код с работающего устройства.", + "Ui.SetupWizard.SelectExisting.ProceedSetupUri": "Продолжить с Setup URI", + "Ui.SetupWizard.SelectExisting.QrOption": "Сканировать QR-код (советуем на телефоне)", + "Ui.SetupWizard.SelectExisting.QrOptionDesc": "Отсканируйте камерой этого устройства QR-код с работающего устройства.", + "Ui.SetupWizard.SelectExisting.Question": "Выберите способ, которым перенести настройки с другого устройства.", + "Ui.SetupWizard.SelectExisting.SetupUriOption": "Использовать Setup URI (советуем)", + "Ui.SetupWizard.SelectExisting.SetupUriOptionDesc": "Вставьте Setup URI, созданный на одном из работающих устройств.", + "Ui.SetupWizard.SelectExisting.Title": "Способ настройки устройства", + "Ui.SetupWizard.SelectNew.Guidance": "Сейчас настроим соединение для синхронизации.", + "Ui.SetupWizard.SelectNew.ManualOption": "Настроить удалённую базу вручную", + "Ui.SetupWizard.SelectNew.ManualOptionDesc": "Вариант для опытных пользователей: если Setup URI нет или нужны подробные настройки. Ещё так настраивают P2P вместо CouchDB или объектного хранилища S3.", + "Ui.SetupWizard.SelectNew.ProceedManual": "Продолжить с ручной настройкой", + "Ui.SetupWizard.SelectNew.ProceedSetupUri": "Продолжить с Setup URI", + "Ui.SetupWizard.SelectNew.Question": "Как настроить это соединение для синхронизации?", + "Ui.SetupWizard.SelectNew.SetupUriOption": "Использовать Setup URI (советуем)", + "Ui.SetupWizard.SelectNew.SetupUriOptionDesc": "Setup URI — это одна строка с данными соединения и входа. Если её выдал скрипт настройки, это простой и безопасный способ настроить всё сразу.", + "Ui.SetupWizard.SelectNew.Title": "Способ соединения", + "Ui.SetupWizard.SetupRemote.BucketOption": "Объектное хранилище, совместимое с S3", + "Ui.SetupWizard.SetupRemote.BucketOptionDesc": "Синхронизация через файлы журнала. Нужно готовое объектное хранилище с поддержкой S3: Amazon S3, MinIO или Cloudflare R2.", + "Ui.SetupWizard.SetupRemote.CouchDbOptionDesc": "Самый подходящий способ синхронизации для нынешней архитектуры плагина. Доступны все возможности. Нужен готовый сервер CouchDB.", + "Ui.SetupWizard.SetupRemote.Guidance": "Выберите тип удалённой базы для этой синхронизации.", + "Ui.SetupWizard.SetupRemote.P2POption": "Между устройствами (P2P)", + "Ui.SetupWizard.SetupRemote.P2POptionDesc": "Устройства синхронизируются напрямую. Сервер не нужен, но оба устройства должны быть в сети одновременно, и часть возможностей может не работать. Интернет нужен только для установки связи, данные через него не идут.", + "Ui.SetupWizard.SetupRemote.ProceedBucket": "Перейти к настройке объектного хранилища", + "Ui.SetupWizard.SetupRemote.ProceedCouchDb": "Перейти к настройке CouchDB", + "Ui.SetupWizard.SetupRemote.ProceedP2P": "Перейти к настройке P2P", + "Ui.SetupWizard.SetupRemote.Title": "Выберите удалённую базу для синхронизации", "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": "Уникальное имя между всеми синхронизируемыми устройствами.", + "Unless you are certain, selecting this options is bit dangerous. It assumes that the server configuration is compatible with this device. If this is not the case, data loss may occur. Please ensure you know what you are doing.": "Если вы не уверены, этот вариант опасен. Он считает, что настройки сервера совместимы с этим устройством. Если это не так, данные могут пропасть. Убедитесь, что понимаете, что делаете.", + "Updating list...": "Обновляем список...", + "URL": "URL", "Use a custom passphrase": "Использовать пользовательскую парольную фразу", "Use a Setup URI (Recommended)": "Использовать Setup URI (рекомендуется)", "Use Custom HTTP Handler": "Использовать пользовательский HTTP обработчик", + "Use Diagnostic RTCPeerConnection for statistics": "Использовать диагностический RTCPeerConnection для статистики", "Use dynamic iteration count": "Использовать динамическое количество итераций", + "Use internal API": "Использовать внутренний API", + "Use Internal API": "Использовать внутренний API", + "Use JWT Authentication": "Использовать аутентификацию JWT", + "Use Path-Style Access": "Доступ в стиле пути (path-style)", + "Use Random Number": "Использовать случайное число", "Use Segmented-splitter": "Использовать сегментный разделитель", "Use splitting-limit-capped chunk splitter": "Использовать разделитель чанков с ограничением", "Use the trash bin": "Использовать корзину", "Use timeouts instead of heartbeats": "Использовать таймауты вместо пульса", + "Use vrtmrz's relay": "Использовать ретранслятор vrtmrz", "username": "имя пользователя", "Username": "Имя пользователя", "Verbose Log": "Подробный лог", "Verify all": "Проверить всё", "Verify and repair all files": "Проверить и восстановить все файлы", "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name.": "Внимание! Это серьёзно повлияет на производительность.", - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.", + "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": "Внимание! Это серьёзно повлияет на производительность. Кроме того, логи не будут синхронизироваться под именем по умолчанию. Пожалуйста, будьте осторожны с логами: они часто содержат вашу конфиденциальную информацию.", + "WATCHING": "СЛЕЖЕНИЕ", + "We can not use \"/\" to the device name": "В имени устройства нельзя использовать «/»", + "We can use only Secure (HTTPS) connections on Obsidian Mobile.": "На Obsidian Mobile работают только защищённые соединения (HTTPS).", "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": "Невозможно изменить имя устройства, пока эта функция включена. Отключите её, чтобы изменить имя устройства.", - "We will now guide you through a few questions to simplify the synchronisation setup.": "Сейчас мы зададим несколько вопросов, чтобы упростить настройку синхронизации。", - "We will now proceed with the server configuration.": "Теперь перейдём к настройке сервера。", + "We have to configure the device name": "Нужно задать имя устройства", + "We recommend that you copy your Vault folder to a safe location. This will provide a safeguard in case a large number of conflicts arise, or if you accidentally synchronise with an incorrect destination.": "Советуем скопировать папку хранилища в надёжное место. Это защитит вас, если появится много конфликтов или вы случайно синхронизируетесь не туда.", + "We will now guide you through a few questions to simplify the synchronisation setup.": "Сейчас мы зададим несколько вопросов, чтобы упростить настройку синхронизации.", + "We will now proceed with the server configuration.": "Теперь перейдём к настройке сервера.", "Welcome to Self-hosted LiveSync": "Добро пожаловать в Self-hosted LiveSync", "When you save a file in the editor, start a sync automatically": "Когда вы сохраняете файл в редакторе, автоматически запускать синхронизацию", "Write credentials in the file": "Записывать учётные данные в файл", @@ -836,5 +1494,9 @@ "xxhash64 (Fastest)": "xxhash64 (самый быстрый)", "Yes, I want to add this device to my existing synchronisation": "Да, я хочу добавить это устройство к существующей синхронизации", "Yes, I want to set up a new synchronisation": "Да, я хочу настроить новую синхронизацию", - "You are adding this device to an existing synchronisation setup.": "Вы добавляете это устройство к существующей настройке синхронизации。" + "You are adding this device to an existing synchronisation setup.": "Вы добавляете это устройство к существующей настройке синхронизации.", + "You can configure in the Obsidian Plugin Settings.": "Настроить это можно в настройках плагина в Obsidian.", + "You should create a new synchronisation destination and rebuild your data there.": "Создайте новое место для синхронизации и пересоберите данные там.", + "You should perform this operation only in exceptional circumstances, such as when the server data is completely corrupted, when changes on all other devices are no longer needed, or when the database size has become unusually large in comparison to the Vault size.": "Делайте это только в исключительных случаях: данные на сервере полностью испорчены, изменения на всех других устройствах больше не нужны или база стала непомерно большой по сравнению с хранилищем.", + "you wanted(Thank you)!": "которую вы хотели (спасибо!)" } diff --git a/src/common/messagesYAML/ru.yaml b/src/common/messagesYAML/ru.yaml index 3a49ac59..393fa851 100644 --- a/src/common/messagesYAML/ru.yaml +++ b/src/common/messagesYAML/ru.yaml @@ -1,18 +1,27 @@ +", please select the option that best describes the current state of your Vault. The application will then check your files in the most appropriate way based on your selection.": + ", выберите вариант, который лучше всего описывает текущее состояние + хранилища. Программа проверит файлы способом, подходящим для этого варианта." (Active): (Активна) (BETA) Always overwrite with a newer file: (БЕТА) Всегда перезаписывать более новым файлом (Beta) Use ignore files: (Бета) Использовать файлы игнорирования (Days passed, 0 to disable automatic-deletion): (Дней прошло, 0 для отключения автоматического удаления) +(e.g., after editing many files whilst offline): (например, после правки многих файлов без сети) +(e.g., immediately after restoring on another computer, or having recovered from a backup): + (например, сразу после восстановления на другом компьютере или из резервной + копии) +(e.g., setting up for the first time on a new smartphone, starting from a clean slate): (например, первая настройка на новом смартфоне, с чистого листа) (ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.: - (ex. Read chunks online) If this option is enabled, LiveSync reads chunks - online directly instead of replicating them locally. Increasing Custom chunk - size is recommended. + (ранее «Read chunks online») Если эта опция включена, LiveSync читает чанки + напрямую в сети, вместо того чтобы реплицировать их локально. Рекомендуется + увеличить пользовательский размер чанка. (MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.: - (MB) If this is set, changes to local and remote files that are larger than - this will be skipped. If the file becomes smaller again, a newer one will be - used. + (МБ) Если задано, изменения локальных и удалённых файлов, размер которых + превышает указанный, будут пропускаться. Если файл снова станет меньше, будет + использована более новая версия. (Mega chars): (Мега символов) +(Missing): (Отсутствует) (Not recommended) If set, credentials will be stored in the file: (Не рекомендуется) Если установлено, учётные данные будут сохранены в файле -(Not recommended) If set, credentials will be stored in the file.: (Not recommended) If set, credentials will be stored in the file. +(Not recommended) If set, credentials will be stored in the file.: (Не рекомендуется) Если установлено, учётные данные будут сохранены в файле. (Obsolete) Use an old adapter for compatibility: (Устарело) Использовать старый адаптер для совместимости (RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.: (RegExp) Оставьте пустым, чтобы синхронизировать все файлы. Укажите регулярное @@ -20,20 +29,93 @@ (RegExp) If this is set, any changes to local and remote files that match this will be skipped.: (RegExp) Если задано, любые изменения локальных и удалённых файлов, соответствующих этому шаблону, будут пропускаться. +(Select this if another device is already using LiveSync.) This option adds this device to that existing synchronisation setup.: + (Выберите этот вариант, если вы уже используете синхронизацию на другом + компьютере или смартфоне.) Он подходит, если вы хотите добавить это устройство + к уже существующей конфигурации LiveSync. +(Select this if you already have another synchronising device.) This option adds this device to the same synchronisation.: + (Выберите, если у вас уже есть другое устройство с синхронизацией.) Этот + вариант добавляет устройство в ту же синхронизацию. +(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.: + (Выберите этот вариант, если настраиваете это устройство как первое устройство + синхронизации.) Он подходит, если вы впервые используете LiveSync и хотите + настроить всё с нуля. +"↑: Overwrite Remote": "↑: Перезаписать удалённую базу" +"↓: Overwrite Local": "↓: Перезаписать локальные данные" +"⇅: Use newer": "⇅: Взять более новое" ++1 week: +1 неделя +"> [!INFO]- The connected devices have been detected as follows:\n${devices}": |- + > [!INFO]- Обнаружены следующие подключённые устройства: + ${devices} +⚠️ Important Notice: ⚠️ Важное предупреждение +⚠️ Please Confirm the Following: ⚠️ Подтвердите следующее +✔ SELECT: ✔ ВЫБРАНО +✔ SYNC: ✔ СИНХРОНИЗАЦИЯ +✔ WATCH: ✔ СЛЕЖЕНИЕ +📡 Off: 📡 Выкл +📡 On: 📡 Вкл +🔴 Disconnected: 🔴 Нет связи +🕵️ Diag: 🕵️ Диагностика +🗑 Delete: 🗑 Удалить +🟢 Connected: 🟢 Есть связь +${count} issue(s) detected!: "Найдено проблем: ${count}!" +A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.: + Setup URI — это одна строка текста, содержащая адрес сервера и данные + аутентификации. Если URI был создан скриптом установки сервера, его + использование обеспечивает простую и безопасную настройку. +Accept: Принять +Accept in session: Принять на сеанс +ACCEPTED: ПРИНЯТО +ACCEPTED (in session): ПРИНЯТО (на сеанс) Access Key: Ключ доступа +Access Key ID: Access Key ID +Action: Действие Activate: Активировать Active Remote Configuration: Активная удалённая конфигурация Add default patterns: Добавить шаблоны по умолчанию Add new connection: Добавить подключение +AddOn Module (ConfigSync) has not been loaded. This is very unexpected situation. Please report this issue.: Модуль ConfigSync не загружен. Так быть не должно. Сообщите об этой ошибке. +AddOn Module (HiddenFileSync) has not been loaded. This is very unexpected situation. Please report this issue.: Модуль HiddenFileSync не загружен. Так быть не должно. Сообщите об этой ошибке. +Advanced: Дополнительно +Advanced Settings: Дополнительные настройки +After restarting, the data on this device will be uploaded to the server as the 'master copy'. Please be aware that any unintended data currently on the server will be completely overwritten.: + После перезапуска данные с этого устройства уйдут на сервер как «главная + копия». Всё, что сейчас лежит на сервере, будет полностью перезаписано. +After restarting, the database on this device will be rebuilt using data from the server. If there are any unsynchronised files in this vault, conflicts may occur with the server data.: + После перезапуска база на этом устройстве пересоберётся из данных сервера. + Если в хранилище есть несинхронизированные файлы, возможны конфликты с данными + сервера. +After that, synchronise to a brand new vault on each other device with the new remote one by one.: + Затем по очереди синхронизируйте каждое другое устройство с новой удалённой + базой в новое пустое хранилище. +All checks passed successfully!: Все проверки пройдены! +All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.: + У всех устройств одинаковое значение прогресса (${progress}). Похоже, ваши + устройства синхронизированы, и можно продолжать Garbage Collection. +All the same or non-existent: Всё одинаково или отсутствует +Allow in session: Разрешить на сеанс +Allow permanently: Разрешить навсегда +Also, please note that if you are using Peer-to-Peer synchronization, this configuration will be used when you switch to other methods and connect to a remote server in the future.: + "Также обратите внимание: если вы используете синхронизацию Peer-to-Peer, эта + конфигурация будет использована, когда вы позже переключитесь на другие методы + и подключитесь к удалённому серверу." Always prompt merge conflicts: Всегда запрашивать разрешение конфликтов слияния Analyse: Анализировать Analyse database usage: Анализ использования базы данных Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.: - Проанализируйте использование базы данных и создайте TSV-отчёт для самостоятельной диагностики. Полученный отчёт можно вставить в любую удобную для вас таблицу. + Проанализируйте использование базы данных и создайте TSV-отчёт для + самостоятельной диагностики. Полученный отчёт можно вставить в любую удобную + для вас таблицу. +Apply All Selected: Применить всё выбранное Apply Latest Change if Conflicting: Применить последнее изменение при конфликте Apply preset configuration: Применить предустановленную конфигурацию +Apply the settings: Применить настройки Ask a passphrase at every launch: Запрашивать парольную фразу при каждом запуске +Auto Connect: Автоподключение +Auto Start P2P Connection: Автозапуск соединения P2P +Automatic: Автоматически Automatically Sync all files when opening Obsidian.: Автоматически синхронизировать все файлы при открытии Obsidian. +Available Peers: Доступные устройства Back: Назад Back to non-configured: Вернуть в состояние без настройки Batch database update: Пакетное обновление базы данных @@ -45,20 +127,45 @@ Before v0.17.16, we used an old adapter for the local database. Now the new adap предпочтителен новый адаптер. Однако требуется перестроение локальной базы данных. Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.: - Before v0.17.16, we used an old adapter for the local database. Now the new - adapter is preferred. However, it needs local database rebuilding. Please - disable this toggle when you have enough time. If leave it enabled, also while - fetching from the remote database, you will be asked to disable this. + До v0.17.16 мы использовали старый адаптер для локальной базы данных. Теперь + предпочтителен новый адаптер. Однако требуется перестроение локальной базы + данных. Пожалуйста, отключите этот переключатель, когда у вас будет достаточно + времени. Если оставить его включённым, отключить его предложат и при загрузке + из удалённой базы данных. +Broadcasting?: Вещание? Bucket Name: Имя бакета +by resetting the remote, you will be informed on other devices.: При сбросе удалённой базы другие устройства получат уведомление. Cancel: Отмена +Cancel Garbage Collection: Отменить Garbage Collection +Changing the encryption algorithm will prevent access to any data previously encrypted with a different algorithm. Ensure that all your devices are configured to use the same algorithm to maintain access to your data.: + Смена алгоритма шифрования закроет доступ к данным, зашифрованным другим + алгоритмом. Задайте один и тот же алгоритм на всех устройствах, чтобы данные + остались доступны. +Changing this setting requires migrating existing data (a bit time may be taken) and restarting Obsidian. Please make sure to back up your data before proceeding.: + Смена этого параметра требует переноса существующих данных (это займёт время) + и перезапуска Obsidian. Сделайте резервную копию до начала. Check: Проверить Check and convert non-path-obfuscated files: Проверить и преобразовать файлы без обфускации пути Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.: Проверяет документы, которые ещё не были преобразованы в path-obfuscated ID, и при необходимости преобразует их. +Check server requirements: Проверить требования к серверу +Checking connection... Please wait.: Проверяем соединение... Подождите. +Chunks: Чанки +Close: Закрыть +Close & Disconnect: Закрыть и отключиться +Close this dialog: Закрыть окно +"Closed:": "Закрыто:" cmdConfigSync: showCustomizationSync: Показать синхронизацию настроек Comma separated `.gitignore, .dockerignore`: Через запятую `.gitignore, .dockerignore` +Command: Команда +Communicating: Идёт обмен данными +Compaction in progress on remote database...: Выполняется компакция удалённой базы данных... +Compaction on remote database completed successfully.: Компакция удалённой базы данных успешно завершена. +Compaction on remote database failed.: Компакция удалённой базы данных завершилась ошибкой. +Compaction on remote database timed out.: Время ожидания компакции удалённой базы данных истекло. +Compare file: Сравнить файл Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.: Сравнивает содержимое файлов между локальной базой данных и хранилищем. Если они не совпадут, вам предложат выбрать, какую версию сохранить. @@ -69,32 +176,62 @@ Compatibility (Metadata): Совместимость (метаданные) Compatibility (Remote Database): Совместимость (удалённая база данных) Compatibility (Trouble addressed): Совместимость (исправленные проблемы) Compute revisions for chunks: Вычислять ревизии для чанков +Configuration: Настройки Configuration Encryption: Шифрование конфигурации Configure: Настроить Configure And Change Remote: Настроить и переключить удалённое хранилище Configure E2EE: Настроить сквозное шифрование Configure Remote: Настроить удалённое хранилище +Configure the same server information as your other devices again, manually, very advanced users only.: + Снова вручную укажите те же параметры сервера, что и на других устройствах. + Только для очень опытных пользователей. +Connect: Подключиться +Connect to existing database and continue: Подключиться к существующей базе и продолжить +"Connected to Signaling Server (as Peer ID: ${peerId})": "Подключено к сигнальному серверу (Peer ID: ${peerId})" +"Connected:": "Подключено:" +Connection Method: Способ подключения +Connection Settings: Настройки подключения +"Connection:": "Соединение:" +Continue anyway: Всё равно продолжить +Continue to CouchDB setup: Перейти к настройке CouchDB +Continue to Peer-to-Peer only setup: Перейти к настройке только Peer-to-Peer +Continue to S3/MinIO/R2 setup: Перейти к настройке S3/MinIO/R2 Copy: Копировать Copy Report to clipboard: Копировать отчёт в буфер обмена +CouchDB Configuration: Настройки CouchDB CouchDB Connection Tweak: Настройки подключения CouchDB +CouchDB validates the database name when you connect. The name must not be empty.: CouchDB проверяет имя базы при подключении. Имя не может быть пустым. +Create or connect to database and continue: Создать базу или подключиться к ней и продолжить +Create P2P remote: Создать удалённую базу P2P Cross-platform: Кроссплатформенные "Current adapter: {adapter}": "Текущий адаптер: {adapter}" +Custom Headers: Свои заголовки Customization Sync: Синхронизация настроек Customization Sync (Beta3): Синхронизация настроек (Beta3) Data Compression: Сжатие данных +Data to Copy: Данные для копирования +Database -> Storage: База данных -> Файлы Database Adapter: Адаптер базы данных Database Name: Имя базы данных Database suffix: Суффикс базы данных +Date: Дата Default: По умолчанию Delay conflict resolution of inactive files: Отложить разрешение конфликтов для неактивных файлов Delay merge conflict prompt for inactive files.: Отложить запрос конфликта слияния для неактивных файлов. Delete: Удалить Delete all customization sync data: Удалить все данные синхронизации настроек Delete all data on the remote server.: Удалить все данные на удалённом сервере. +Delete All of: Удалить все Delete local database to reset or uninstall Self-hosted LiveSync: Удалить локальную базу данных, чтобы сбросить или удалить Self-hosted LiveSync Delete old metadata of deleted files on start-up: Удалять старые метаданные удалённых файлов при запуске Delete Remote Configuration: Удалить удалённую конфигурацию Delete remote configuration '{name}'?: Удалить удалённую конфигурацию '{name}'? +Delete remote configuration '${name}'?: Удалить настройку удалённой базы «${name}»? +DENIED: ОТКАЗАНО +DENIED (in session): ОТКАЗАНО (на сеанс) +Deny: Отклонить +Deny in session: Отклонить на сеанс +Deny permanently: Отклонить навсегда descConnectSetupURI: Это рекомендуемый способ настройки Self-hosted LiveSync с помощью Setup URI. descCopySetupURI: Идеально для настройки нового устройства! descEnableLiveSync: Включайте это только после настройки одного из двух вариантов выше. @@ -102,9 +239,20 @@ descFetchConfigFromRemote: Загрузить необходимые настр descManualSetup: Не рекомендуется, но полезно, если у вас нет Setup URI descTestDatabaseConnection: Открыть подключение к базе данных. descValidateDatabaseConfig: Проверяет и исправляет потенциальные проблемы с конфигурацией базы данных. +Deselect all: Снять выбор со всех desktop: рабочий стол +Detected Peers: Обнаруженные устройства Developer: Разработчик +Device: Устройство +device name: имя устройства Device name: Имя устройства +Device name to identify the device. Please use shorter one for the stable peer detection, i.e., "iphone-16" or "macbook-2021".: + Имя, по которому видно устройство. Берите короткое имя для надёжного + обнаружения устройств, например «iphone-16» или «macbook-2021». +Device Peer ID: Peer ID устройства +Device Setup Method: Способ настройки устройства +"Devices:": "Устройства:" +Diagnostic RTCPeerConnection is enabled: Диагностика RTCPeerConnection включена dialog: yourLanguageAvailable: _value: >- @@ -120,10 +268,14 @@ dialog: Надеемся, вам будет удобно использовать! btnRevertToDefault: Оставить lang-def Title: Доступен перевод! +Diff: Различия +Different: Отличается Disables all synchronization and restart.: Отключает всю синхронизацию и перезапускает приложение. Disables logging, only shows notifications. Please disable if you report an issue.: Отключает логирование, показывает только уведомления. Пожалуйста, отключите при сообщении о проблеме. +Disconnect: Отключиться +Dismiss: Скрыть Display Language: Язык интерфейса Display name: Отображаемое имя Do not check configuration mismatch before replication: Не проверять несовпадение конфигурации перед репликацией @@ -140,7 +292,7 @@ Doctor: Yes: Да Dialogue: Main: >- - Привет! Диагностика настроек активирована из-за activateReason! + Привет! Диагностика настроек активирована из-за ${activateReason}! К сожалению, некоторые настройки были обнаружены как потенциальные проблемы. @@ -151,29 +303,30 @@ Doctor: Сообщаем вам заранее, мы спросим о следующих пунктах. - issues + ${issues} Начнём? MainFix: |- - name + + ## ${name} | Текущее | Идеальное | |:---:|:---:| - | current | ideal | + | ${current} | ${ideal} | - **Уровень рекомендации:** level + **Уровень рекомендации:** ${level} ### Почему это было обнаружено? - reason + ${reason} - note + ${note} Исправить на идеальное значение? Title: Диагностика Self-hosted LiveSync TitleAlmostDone: Почти готово! - TitleFix: Исправление проблемы current/total + TitleFix: Исправление проблемы ${current}/${total} Level: Must: Обязательно Necessary: Необходимо @@ -189,6 +342,7 @@ Doctor: E2EE_V02500: REASON: Сквозное шифрование стало более надёжным и быстрым. Предыдущее E2EE было скомпрометировано. Следует применить как можно скорее. +Document History: История документа Duplicate: Дублировать Duplicate remote: Дублировать удалённую конфигурацию E2EE Configuration: Конфигурация сквозного шифрования @@ -200,45 +354,145 @@ Enable advanced features: Включить расширенные функции Enable customization sync: Включить синхронизацию настроек Enable Developers' Debug Tools.: Включить инструменты разработчика. Enable edge case treatment features: Включить функции обработки граничных случаев +Enable P2P Replicator: Включить репликатор P2P Enable poweruser features: Включить функции для опытных пользователей Enable this if your Object Storage doesn't support CORS: Включите, если ваше объектное хранилище не поддерживает CORS Enable this option to automatically apply the most recent change to documents even when it conflicts: Включите эту опцию для автоматического применения последних изменений к документам даже при конфликте +Enabled: Включено +Enabling end-to-end encryption ensures that your data is encrypted on your device before being sent to the remote server. This means that even if someone gains access to the server, they won't be able to read your data without the passphrase. Make sure to remember your passphrase, as it will be required to decrypt your data on other devices.: + "Сквозное шифрование шифрует данные на устройстве до отправки на удалённый + сервер. Даже если кто-то получит доступ к серверу, без парольной фразы он не + прочитает данные. Запомните парольную фразу: она нужна, чтобы расшифровать + данные на других устройствах." Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.: Шифровать содержимое на удалённой базе данных. Рекомендуется включить при использовании функции синхронизации плагина. Encrypting sensitive configuration items: Шифрование конфиденциальных настроек +Encryption Algorithm: Алгоритм шифрования Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.: Парольная фраза шифрования. При изменении нужно перезаписать базу данных сервера новыми (зашифрованными) файлами. End-to-End Encryption: Сквозное шифрование Endpoint URL: URL конечной точки Enhance chunk size: Улучшить размер чанка +Enter a complete HTTP or HTTPS URL.: Введите полный адрес HTTP или HTTPS. +Enter a folder prefix (optional): Введите префикс папки (необязательно) +Enter Server Information: Ввести данные сервера +Enter Setup URI: Введите Setup URI +Enter the server information manually: Ввести данные сервера вручную +Enter TURN credential: Введите пароль TURN +Enter TURN username: Введите имя пользователя TURN +Enter your Access Key ID: Введите свой Access Key ID +Enter your Bucket Name: Введите имя бакета +Enter your database name: Введите имя базы +Enter your JWT Key ID: Введите свой JWT Key ID +Enter your JWT secret or private key: Введите секрет JWT или закрытый ключ +Enter your JWT Subject (CouchDB Username): Введите JWT Subject (имя пользователя CouchDB) +Enter your passphrase: Введите парольную фразу +Enter your password: Введите пароль +Enter your Region (e.g., us-east-1, auto for R2): Введите регион (например, us-east-1; для R2 — auto) +Enter your Secret Access Key: Введите свой Secret Access Key +Enter your username: Введите имя пользователя +"Error during connection test: ${reason}": "Ошибка при проверке соединения: ${reason}" +"Error during testAndFixSettings: ${reason}": "Ошибка в testAndFixSettings: ${reason}" +Experimental Settings: Экспериментальные настройки Export: Экспорт +Failed to connect to remote for compaction.: Не удалось подключиться к удалённой базе данных для компакции. +Failed to connect to remote for compaction. ${reason}: Не удалось подключиться к удалённой базе данных для компакции. ${reason} +"Failed to connect to the server: ${reason}": "Не удалось подключиться к серверу: ${reason}" +Failed to connect to the server. Please check your settings.: Не удалось подключиться к серверу. Проверьте настройки. +"Failed to connect to the signalling relay: ${reason}": "Не удалось подключиться к сигнальному ретранслятору: ${reason}" +Failed to create replicator instance.: Не удалось создать репликатор. +Failed to parse Setup-URI.: Не удалось разобрать Setup-URI. +Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.: + Не удалось запустить одноразовую репликацию перед Garbage Collection. Garbage + Collection отменена. +Failed to start replication after Garbage Collection.: Не удалось запустить репликацию после Garbage Collection. +"Failed:": "Сбоев:" Fetch: Получить Fetch chunks on demand: Загружать чанки по требованию Fetch database with previous behaviour: Загрузить базу данных с предыдущим поведением Fetch remote settings: Получить настройки с удалённого хранилища +FETCHING: ЗАГРУЗКА +Fetching status...: Получаем состояние... +File integrity: Целостность файлов File to resolve conflict: Файл для разрешения конфликта +File to view History: Файл для просмотра истории Filename: Имя файла +"Final Confirmation: Overwrite Server Data with This Device's Files": "Последнее подтверждение: перезаписать данные сервера файлами этого устройства" +First, please select the option that best describes your current situation.: Сначала выберите вариант, который лучше всего описывает вашу текущую ситуацию. +Fix: Исправить Flag and restart: Пометить и перезапустить +Flagged Selective: Помеченные выборочно +Folder Prefix: Префикс папки +For HS256/HS512 algorithms, provide the shared secret key. For ES256/ES512 algorithms, provide the pkcs8 PEM-formatted private key.: + Для алгоритмов HS256/HS512 укажите общий секретный ключ. Для алгоритмов + ES256/ES512 укажите закрытый ключ в формате pkcs8 PEM. Forces the file to be synced when opened.: Принудительно синхронизировать файл при открытии. Fresh Start Wipe: Полный сброс для чистого старта +Furthermore, if conflicts are already present in the server data, they will be synchronised to this device as they are, and you will need to resolve them locally.: + Кроме того, конфликты, которые уже есть на сервере, придут на это устройство + как есть, и разбирать их придётся здесь. +Garbage Collection cancelled by user.: Пользователь отменил Garbage Collection. +"Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": + "Garbage Collection завершена. Удалено чанков: ${deletedChunks} / + ${totalChunks}. Затраченное время: ${seconds} сек." +Garbage Collection Confirmation: Подтверждение Garbage Collection Garbage Collection V3 (Beta): Сборка мусора V3 (Beta) +"Garbage Collection: Found ${unusedChunks} unused chunks to delete.": + "Garbage Collection: найдено ${unusedChunks} неиспользуемых чанков для + удаления." +"Garbage Collection: Scanned ${scanned} / ~${docCount}": "Garbage Collection: просканировано ${scanned} / ~${docCount}" +"Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": + "Garbage Collection: сканирование завершено. Всего чанков: ${totalChunks}, + используемых чанков: ${usedChunks}" +Gathering information...: Собираем сведения... +Generate Random ID: Создать случайный ID +Group ID: ID группы Handle files as Case-Sensitive: Обрабатывать файлы с учётом регистра +Have you created a backup before proceeding?: Вы сделали резервную копию перед началом? +Hidden file synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.: + Синхронизация скрытых файлов временно выключена. Включите её после того, как + заберёте данные с сервера, если она вам нужна. Hidden Files: Скрытые файлы +Hide completely: Скрыть полностью +Hide not applicable items: Скрыть неподходящие пункты +Hide password: Скрыть пароль +Higher (${local} > ${remote}): Больше (${local} > ${remote}) +Highlight diff: Подсветить различия How to display network errors when the sync server is unreachable.: Определяет, как отображать сетевые ошибки, если сервер синхронизации недоступен. +How would you like to configure the connection to your server?: Как вы хотите настроить подключение к серверу? +However, This should not be enabled if you want to increase your secrecy more.: Но если вам нужна повышенная скрытность, не включайте это. +I am adding a device to an existing synchronisation setup: Я добавляю устройство к существующей настройке синхронизации +I am setting this up for the first time: Я настраиваю это впервые +I am setting up a new server for the first time / I want to reset my existing server.: Я настраиваю новый сервер впервые / хочу сбросить существующий сервер. +I am unable to create a backup of my Vault.: Я не могу сделать резервную копию хранилища. +I am unable to create a backup of my Vaults.: Я не могу сделать резервные копии хранилищ. +I have created a backup of my Vault.: Я сделал резервную копию хранилища. +I know my server details, let me enter them: Я знаю параметры сервера, позвольте ввести их вручную +I understand that all changes made on other smartphones or computers possibly could be lost.: Я понимаю, что изменения на других смартфонах и компьютерах могут пропасть. +I understand that other devices will no longer be able to synchronise, and will need to be reset the synchronisation information.: + Я понимаю, что другие устройства перестанут синхронизироваться и на них надо + будет сбросить данные синхронизации. +I understand that this action is irreversible once performed.: Я понимаю, что это действие нельзя отменить. +I understand the risks and will proceed without a backup.: Я понимаю риски и продолжу без резервной копии. +I Understand, Overwrite Server: Понимаю, перезаписать сервер +If "Auto Start P2P Connection" is enabled, the P2P connection will be started automatically when the plug-in launches.: + Если включён параметр «Автозапуск соединения P2P», соединение P2P запускается + при старте плагина. If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).: Если отключено, чанки будут разделяться в основном потоке. If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this.: Если включено, будет использоваться эффективная синхронизация настроек для каждого файла. If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.: - If enabled per-filed efficient customization sync will be used. We need a - small migration when enabling this. And all devices should be updated to - v0.23.18. Once we enabled this, we lost a compatibility with old versions. + Если включено, будет использоваться эффективная синхронизация настроек для + каждого файла. При включении потребуется небольшая миграция. Все устройства + должны быть обновлены до v0.23.18. После включения совместимость со старыми + версиями будет потеряна. If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.: Если включено, чанки будут разделены не более чем на 100 элементов. If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.: Если включено, вновь созданные чанки временно хранятся в документе. If enabled, the ⛔ icon will be shown inside the status instead of the file warnings banner. No details will be shown.: Если включено, значок будет показан внутри статуса. @@ -249,28 +503,87 @@ If this enabled, All files are handled as case-Sensitive (Previous behaviour).: If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.: Если включено, чанки будут разделены на семантически значимые сегменты. If this is set, changes to local files which are matched by the ignore files will be skipped.: Если установлено, изменения файлов из списка игнорирования будут пропущены. If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.: - If this is set, changes to local files which are matched by the ignore files - will be skipped. Remote changes are determined using local ignore files. + Если установлено, изменения файлов из списка игнорирования будут пропущены. + Удалённые изменения определяются по локальным файлам игнорирования. If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely.: Если эта опция включена, PouchDB будет держать соединение открытым 60 секунд. If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.: - If this option is enabled, PouchDB will hold the connection open for 60 - seconds, and if no change arrives in that time, close and reopen the socket, - instead of holding it open indefinitely. Useful when a proxy limits request - duration but can increase resource usage. + Если эта опция включена, PouchDB будет держать соединение открытым 60 секунд + и, если за это время не придёт ни одного изменения, закроет и откроет сокет + заново, вместо того чтобы держать соединение открытым бесконечно. Полезно, + когда прокси ограничивает длительность запроса, но может увеличить расход + ресурсов. +If you cannot avoid CORS issues, you might want to try this option. It uses Obsidian's internal API to communicate with the CouchDB server. Not compliant with web standards, but works. Note that this might break in future Obsidian versions.: + "Если не удаётся обойти проблемы CORS, попробуйте этот параметр. Он использует + внутренний API Obsidian для связи с сервером CouchDB. Это не по + веб-стандартам, но работает. Учтите: в новых версиях Obsidian это может + перестать работать." +If you cannot avoid CORS issues, you might want to try this option. It uses Obsidian's internal API to communicate with the S3 server. Not compliant with web standards, but works. Note that this might break in future Obsidian versions.: + "Если не удаётся обойти проблемы CORS, попробуйте этот параметр. Он использует + внутренний API Obsidian для связи с сервером S3. Это не по веб-стандартам, но + работает. Учтите: в новых версиях Obsidian это может перестать работать." +If you have unsynchronised changes in your Vault on this device, they will likely diverge from the server's versions after the reset. This may result in a large number of file conflicts.: + Если в хранилище на этом устройстве есть несинхронизированные изменения, после + сброса они разойдутся с версиями на сервере. Тогда появится много конфликтов + файлов. +If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value.: + Если при работе с IBM Cloudant вы упёрлись в предел размера запроса, уменьшите + размер пакета и лимит пакета. +If you understand the risks and still wish to proceed, select so.: Если вы понимаете риски и всё равно хотите продолжить, выберите этот вариант. +If you want to store the data in a specific folder within the bucket, you can specify a folder prefix here. Otherwise, leave it blank to store data at the root of the bucket.: + Чтобы данные лежали в отдельной папке внутри бакета, укажите здесь префикс + папки. Оставьте поле пустым, и данные лягут в корень бакета. +If you want to use `LiveSync`, you should broadcast changes. All `watching` peers which detects this will start the replication for fetching.: + Для работы `LiveSync` включите вещание изменений. Все устройства в режиме + `watching` заметят это и запустят репликацию, чтобы забрать данные с сервера. +Ignore: Пропустить +Ignore and Proceed: Игнорировать и продолжить Ignore files: Файлы для игнорирования Ignore patterns: Шаблоны исключения Import connection: Импортировать подключение +IMPORTANT: ВАЖНО +In most cases, you should stick with the default algorithm (${algorithm}), This setting is only required if you have an existing Vault encrypted in a different format.: + В большинстве случаев оставьте алгоритм по умолчанию (${algorithm}). Этот + параметр нужен, только если у вас уже есть хранилище, зашифрованное в другом + формате. +In this scenario, Self-hosted LiveSync will recreate metadata for every file and deliberately generate conflicts. Where the file content is identical, these conflicts will be resolved automatically.: + В этом случае Self-hosted LiveSync заново создаст метаданные для каждого файла + и намеренно создаст конфликты. Там, где содержимое файлов совпадает, конфликты + решатся сами. +"Incoming:": "Входящие:" Incubate Chunks in Document: Инкубировать чанки в документе +Initial Action: Первое действие Initialise all journal history, On the next sync, every item will be received and sent.: Инициализирует всю историю журнала. При следующей синхронизации каждый элемент будет заново получен и отправлен. +Initialise journal received history. On the next sync, every item except this device sent will be downloaded again.: + Обнуляет историю полученных записей журнала. При следующей синхронизации всё, + кроме отправленного этим устройством, будет загружено заново. +Initialise journal sent history. On the next sync, every item except this device received will be sent again.: + Обнуляет историю отправленных записей журнала. При следующей синхронизации + всё, кроме полученного этим устройством, будет отправлено заново. Interval (sec): Интервал (сек) +INVERTED: ИНВЕРТИРОВАНО +"Issue detection log:": "Журнал поиска проблем:" +It is strongly advised to create a backup before proceeding. Continuing without a backup may lead to data loss.: + Настоятельно советуем сделать резервную копию. Без неё вы можете потерять + данные. +Just for a minute, please!: Одну минуту! +JWT (JSON Web Token) authentication allows you to securely authenticate with the CouchDB server using tokens. Ensure that your CouchDB server is configured to accept JWTs and that the provided key and settings match the server's configuration. Incidentally, I have not verified it very thoroughly.: + Аутентификация JWT (JSON Web Token) позволяет безопасно входить на сервер + CouchDB по токенам. Проверьте, что сервер CouchDB принимает JWT и что ключ и + настройки совпадают с настройками сервера. К слову, это проверено не очень + тщательно. +JWT Algorithm: Алгоритм JWT +JWT Expiration Duration (minutes): Срок жизни JWT (минуты) +JWT Key: Ключ JWT +JWT Key ID (kid): JWT Key ID (kid) +JWT Subject (sub): JWT Subject (sub) K: exp: Экспериментальная long_p2p_sync: title_p2p_sync P2P: Peer-к-Peer Peer: Устройство - ScanCustomization: Scan customization + ScanCustomization: Сканировать настройки short_p2p_sync: P2P Синхр. title_p2p_sync: Синхронизация между устройствами Keep empty folder: Сохранять пустые папки @@ -279,6 +592,7 @@ lang-de: Deutsch lang-def: lang_def lang-es: Español lang-fr: Français +lang-he: Иврит lang-ja: 日本語 lang-ko: 한국어 lang-ru: Русский @@ -286,6 +600,7 @@ lang-zh: 简体中文 lang-zh-tw: 繁體中文 Later: Позже "Limit: {datetime} ({timestamp})": "Лимит: {datetime} ({timestamp})" +live: живая синхронизация LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.: LiveSync не может обработать несколько хранилищ с одинаковым именем без разных префиксов. @@ -294,16 +609,18 @@ liveSyncReplicator: cantReplicateLowerValue: Нельзя реплицировать с меньшим значением. checkingLastSyncPoint: Поиск последней точки синхронизации. couldNotConnectTo: |- - Не удалось подключиться к uri : name - (db) - couldNotConnectToRemoteDb: "Не удалось подключиться к удалённой базе данных: d" + Не удалось подключиться к ${uri} : ${name} + (${db}) + couldNotConnectToRemoteDb: "Не удалось подключиться к удалённой базе данных: ${d}" couldNotConnectToServer: Не удалось подключиться к серверу. - couldNotConnectToURI: Не удалось подключиться к uri:dbRet + couldNotConnectToURI: Не удалось подключиться к ${uri}:${dbRet} couldNotMarkResolveRemoteDb: Не удалось отметить удалённую базу данных как разрешённую. liveSyncBegin: Начало LiveSync... lockRemoteDb: Блокировка удалённой базы данных для предотвращения повреждения данных markDeviceResolved: Отметить это устройство как «разрешённое». - oneShotSyncBegin: Начало OneShot синхронизации... (syncMode) + mismatchedTweakDetected: Настройки на устройствах разошлись. Запустите + репликацию вручную — плагин попробует это исправить. + oneShotSyncBegin: Начало OneShot синхронизации... (${syncMode}) remoteDbCorrupted: Удалённая база данных новее или повреждена, убедитесь, что установлена последняя версия self-hosted-livesync remoteDbCreatedOrConnected: Удалённая база данных создана или подключена @@ -312,15 +629,16 @@ liveSyncReplicator: remoteDbMarkedResolved: Удалённая база данных отмечена как разрешённая. replicationClosed: Репликация закрыта replicationInProgress: Репликация уже выполняется - retryLowerBatchSize: "Повтор с меньшим размером пакета: batch_size/batches_limit" + retryLowerBatchSize: "Повтор с меньшим размером пакета: ${batch_size}/${batches_limit}" unlockRemoteDb: Разблокировка удалённой базы данных для предотвращения повреждения данных liveSyncSetting: - errorNoSuchSettingItem: "Такого параметра настройки не существует: key" - originalValue: "Оригинал: value" - valueShouldBeInRange: Значение должно быть min < значение < max + errorNoSuchSettingItem: "Такого параметра настройки не существует: ${key}" + originalValue: "Оригинал: ${value}" + valueShouldBeInRange: Значение должно быть ${min} < значение < ${max} liveSyncSettings: btnApply: Применить Local Database Tweak: Настройки локальной базы данных +Local only: Только локально Lock: Заблокировать Lock Server: Заблокировать сервер Lock the remote server to prevent synchronization with other devices.: @@ -332,6 +650,9 @@ logPane: pause: Пауза title: Лог Self-hosted LiveSync wrap: Перенос +Lower (${local} < ${remote}): Меньше (${local} < ${remote}) +Maintenance Commands: Команды обслуживания +Maintenance mode: Режим обслуживания Maximum delay for batch database updating: Максимальная задержка пакетного обновления базы данных Maximum file size: Максимальный размер файла Maximum Incubating Chunk Size: Максимальный размер инкубируемого чанка @@ -344,22 +665,43 @@ Memory cache size (by total items): Размер кэша памяти (по о Merge: Объединить Minimum delay for batch database updating: Минимальная задержка пакетного обновления базы данных Minimum interval for syncing: Минимальный интервал синхронизации +Mixed: Смешано moduleCheckRemoteSize: logCheckingStorageSizes: Проверка размеров хранилища - logCurrentStorageSize: "Размер удалённого хранилища: measuredSize" - logExceededWarning: "Размер удалённого хранилища: measuredSize превысил notifySize" - logThresholdEnlarged: Порог увеличен до sizeМБ + logCurrentStorageSize: "Размер удалённого хранилища: ${measuredSize}" + logExceededWarning: "Размер удалённого хранилища: ${measuredSize} превысил ${notifySize}" + logThresholdEnlarged: Порог увеличен до ${size} МБ msgConfirmRebuild: Это может занять некоторое время. Вы действительно хотите перестроить всё сейчас? - msgDatabaseGrowing: Ваша база данных увеличивается! Но не волнуйтесь, мы можем решить это сейчас. + msgDatabaseGrowing: | + **Ваша база данных увеличивается!** Но не волнуйтесь, мы можем решить это сейчас. Это время до того, как на удалённом хранилище закончится место. + + | Измеренный размер | Заданный размер | + | --- | --- | + | ${estimatedSize} | ${maxSize} | + + > [!MORE]- + > Если вы пользуетесь плагином много лет, в базе данных могли накопиться чанки без ссылок — то есть мусор. Поэтому мы рекомендуем перестроить всё. Вероятно, база станет намного меньше. + > + > Если объём вашего хранилища просто растёт, лучше перестроить всё после наведения порядка в файлах. Self-hosted LiveSync не удаляет сами данные, даже если вы их удалили, — так работа идёт быстрее. Это в общих чертах [описано в документации](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md). + > + > Если рост вас не смущает, вы можете увеличить порог уведомления на 100 МБ. Так стоит делать, если вы работаете на своём сервере. Но время от времени всё же лучше перестраивать всё. + > + + > [!WARNING] + > Если вы перестраиваете всё, убедитесь, что все устройства синхронизированы. Впрочем, плагин объединит столько, сколько сможет. msgSetDBCapacity: Можно установить предупреждение о максимальной ёмкости базы данных. + noticeExceeded: Размер удалённого хранилища ${measuredSize}, это больше + заданного порога уведомления ${notifySize}. {HERE} + noticeNotConfigured: Уведомления о размере удалённого хранилища не настроены. {HERE} option2GB: 2ГБ (Стандарт) option800MB: 800МБ (Cloudant, fly.io) optionAskMeLater: Спросить позже optionDismiss: Отклонить - optionIncreaseLimit: увеличить до newMaxМБ + optionIncreaseLimit: увеличить до ${newMax} МБ optionNoWarn: Нет, не уведомлять optionRebuildAll: Перестроить всё сейчас + optionReview: Посмотреть варианты titleDatabaseSizeLimitExceeded: Размер удалённого хранилища превысил лимит titleDatabaseSizeNotify: Настройка уведомления о размере базы данных moduleInputUIObsidian: @@ -371,7 +713,7 @@ moduleLiveSyncMain: logAdditionalSafetyScan: Дополнительная проверка безопасности... logLoadingPlugin: Загрузка плагина... logPluginInitCancelled: Инициализация плагина отменена модулем - logPluginVersion: Self-hosted LiveSync vmanifestVersion packageVersion + logPluginVersion: Self-hosted LiveSync v${manifestVersion} ${packageVersion} logReadChangelog: LiveSync обновлён, пожалуйста, прочитайте список изменений! logSafetyScanCompleted: Дополнительная проверка безопасности завершена logSafetyScanFailed: Дополнительная проверка безопасности не удалась в модуле @@ -380,37 +722,39 @@ moduleLiveSyncMain: автоматическая синхронизация временно отключена. Убедитесь, что все устройства обновлены перед включением. msgScramEnabled: > - Self-hosted LiveSync has been configured to ignore some events. Is this - correct? + Self-hosted LiveSync настроен игнорировать некоторые события. Это верно? - | Type | Status | Note | + | Тип | Статус | Примечание | |:---:|:---:|---| - | Storage Events | ${fileWatchingStatus} | Every modification will be - ignored | + | События хранилища | ${fileWatchingStatus} | Любое изменение будет + проигнорировано | - | Database Events | ${parseReplicationStatus} | Every synchronised change - will be postponed | + | События базы данных | ${parseReplicationStatus} | Любое синхронизированное + изменение будет отложено | - Do you want to resume them and restart Obsidian? + Возобновить их и перезапустить Obsidian? > [!DETAILS]- - > These flags are set by the plug-in while rebuilding, or fetching. If the - process ends abnormally, it may be kept unintended. + > Эти флаги устанавливает плагин во время перестроения или загрузки. Если + процесс завершится аварийно, они могут остаться включёнными непреднамеренно. - > If you are not sure, you can try to rerun these processes. Make sure to - back your vault up. + > Если вы не уверены, попробуйте выполнить эти процессы заново. Обязательно + сделайте резервную копию хранилища. optionKeepLiveSyncDisabled: Оставить LiveSync отключённым optionResumeAndRestart: Продолжить и перезапустить Obsidian titleScramEnabled: Экстренная остановка включена moduleLocalDatabase: logWaitingForReady: Ожидание готовности... moduleLog: + pathComponentTooLong: В этом пути есть имя файла или папки длиннее ${maxBytes} + байт UTF-8. На некоторых файловых системах Android и Linux оно может не + работать. showLog: Показать лог moduleMigration: fix0256: @@ -418,8 +762,42 @@ moduleMigration: checkItLater: Проверить позже DismissForever: Исправлено, больше не спрашивать fix: Исправить - message: Из-за недавней ошибки некоторые файлы могут быть неправильно сохранены. - messageUnrecoverable: "Файлы не могут быть исправлены на этом устройстве:" + message: > + Из-за недавней ошибки (в v0.25.6) некоторые файлы могли быть неправильно + сохранены в базе данных синхронизации. + + Мы проверили файлы и нашли те, которые нужно исправить. + + + **Файлы, готовые к исправлению:** + + + ${files} + + + У этих файлов в хранилище есть исходный файл совпадающего размера, поэтому + они, скорее всего, восстановимы. + + Мы можем использовать их, чтобы исправить базу данных. Нажмите кнопку + «Исправить» ниже. + + + ${messageUnrecoverable} + + + Если вы захотите запустить это снова, вы можете сделать это из Hatch. + messageUnrecoverable: > + **Файлы не могут быть исправлены на этом устройстве:** + + + ${filesNotRecoverable} + + + У этих файлов несогласованные метаданные, и на этом устройстве их + исправить нельзя (чаще всего мы не можем определить, какие данные верны). + + Чтобы восстановить их, проверьте другие свои устройства (тоже этой + функцией) или восстановите файлы вручную из резервной копии. title: Обнаружены повреждённые файлы insecureChunkExist: buttons: @@ -433,15 +811,38 @@ moduleMigration: была повреждена. Приносим извинения. Автоматически отключено. logFetchRemoteTweakFailed: Не удалось загрузить удалённые настройки logLocalDatabaseNotReady: Что-то пошло не так! Локальная база данных не готова - logMigratedSameBehaviour: Миграция на db:current с тем же поведением, что и раньше - logMigrationFailed: Миграция не удалась или отменена с old на current + logMigratedSameBehaviour: Миграция на db:${current} с тем же поведением, что и раньше + logMigrationFailed: Миграция не удалась или отменена с ${old} на ${current} logRedflag2CreationFail: Не удалось создать redflag2 logRemoteTweakUnavailable: Не удалось получить удалённые настройки logSetupCancelled: Настройка отменена, Self-hosted LiveSync ожидает вашей настройки! msgFetchRemoteAgain: Удалённая база данных, похоже, уже была мигрирована. Конфигурация этого устройства несовместима. - msgSinceV02321: Начиная с v0.23.21, self-hosted LiveSync изменил поведение и - структуру базы данных. + msgSinceV02321: >- + Начиная с v0.23.21, self-hosted LiveSync изменил поведение по умолчанию и + структуру базы данных. Внесены следующие изменения: + + + 1. **Чувствительность имён файлов к регистру** + Теперь имена файлов обрабатываются без учёта регистра. Для большинства платформ это полезное изменение, кроме Linux и iOS, которые плохо работают с регистром имён файлов. + (На них для файлов с одинаковым именем в разном регистре будет показано предупреждение). + + 2. **Обработка ревизий чанков** + Чанки неизменяемы, поэтому их ревизии можно зафиксировать. Это изменение ускорит сохранение файлов. + + ___Однако, чтобы включить любое из этих изменений, нужно перестроить и + удалённую, и локальную базы данных. Это занимает несколько минут, и мы + рекомендуем делать это, когда у вас есть достаточно времени.___ + + + - Если вы хотите сохранить прежнее поведение, вы можете пропустить этот + процесс кнопкой `${KEEP}`. + + - Если времени не хватает, выберите `${DISMISS}`. Мы спросим вас позже. + + - Если вы уже перестроили базу данных на другом устройстве, выберите + `${DISMISS}` и попробуйте синхронизироваться снова. Расхождение обнаружено, + поэтому вопрос появится снова. optionAdjustRemote: Настроить под удалённую optionDecideLater: Решить позже optionEnableBoth: Включить оба @@ -454,24 +855,54 @@ moduleMigration: moduleObsidianMenu: replicate: Реплицировать More actions: Другие действия +"Mostly Complete: Decision Required": "Почти готово: нужно ваше решение" Move remotely deleted files to the trash, instead of deleting.: Перемещать удалённые на удалённом сервере файлы в корзину вместо удаления. +My remote server is already set up. I want to join this device.: Мой удалённый сервер уже настроен. Хочу подключить это устройство. +Name: Имя Network warning style: Стиль сетевого предупреждения +NEW: НОВОЕ New Remote: Новое удалённое хранилище +Newer (${diff}): Новее (${diff}) +No checks have been performed yet.: Проверки ещё не выполнялись. +No connected device information found. Cancelling Garbage Collection.: + Не найдена информация о подключённых устройствах. Garbage Collection + отменяется. +No Connection: Нет соединения +No devices available. Waiting for other devices to connect...: Устройств нет. Ждём подключения других устройств... +No Items.: Записей нет. No limit configured: Лимит не задан +NO PREVIEW: НЕТ ПРЕДПРОСМОТРА +No, please take me back: Нет, верните меня назад +Node ID: ID узла +Node Information Missing: Отсутствует информация об узле Non-Synchronising files: Несинхронизируемые файлы Normal Files: Обычные файлы Not all messages have been translated. And, please revert to "Default" when reporting errors.: Не все сообщения переведены. И, пожалуйста, вернитесь к «По умолчанию» при сообщении об ошибках. +Not configured: Не настроено +Not now: Не сейчас +Note that the Group ID is not limited to the generated format; you can use any string as the Group ID.: "ID группы не обязан совпадать с созданным форматом: подойдёт любая строка." +Note that you can generate a new Setup URI by running the "Copy settings as a new Setup URI" command in the command palette.: + Новый Setup URI можно создать, выполнив в палитре команд команду «Скопировать + настройки как новый Setup URI». Notify all setting files: Уведомлять обо всех файлах настроек Notify customized: Уведомлять о настройках Notify when other device has newly customized.: Уведомлять, когда другое устройство изменило настройки. Notify when the estimated remote storage size exceeds on start up: Уведомлять, когда оценочный размер удалённого хранилища превышает при запуске Number of batches to process at a time. Defaults to 40. Minimum is 2.: Количество пакетов для обработки за раз. По умолчанию 40. Минимум 2. Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.: - Number of batches to process at a time. Defaults to 40. Minimum is 2. This - along with batch size controls how many docs are kept in memory at a time. + Количество пакетов для обработки за раз. По умолчанию 40. Минимум 2. Вместе с + размером пакета это определяет, сколько документов одновременно хранится в + памяти. Number of changes to sync at a time. Defaults to 50. Minimum is 2.: Количество изменений для синхронизации за раз. По умолчанию 50. Минимум 2. +Obfuscate Properties: Скрывать свойства файлов +Obfuscating properties (e.g., path of file, size, creation and modification dates) adds an additional layer of security by making it harder to identify the structure and names of your files and folders on the remote server. This helps protect your privacy and makes it more difficult for unauthorized users to infer information about your data.: + "Скрытие свойств (путь к файлу, размер, даты создания и изменения) добавляет + ещё один уровень защиты: по данным на сервере труднее понять структуру и имена + ваших файлов и папок. Это бережёт вашу личную информацию и мешает посторонним + узнать что-то о ваших данных." +Obsidian version: Версия Obsidian obsidianLiveSyncSettingTab: btnApply: Применить btnCheck: Проверить @@ -488,11 +919,15 @@ obsidianLiveSyncSettingTab: defaultLanguage: По умолчанию descConnectSetupURI: Это рекомендуемый способ настройки Self-hosted LiveSync с помощью Setup URI. descCopySetupURI: Идеально подходит для настройки нового устройства! - descEnableLiveSync: Включайте это только после настройки одного из двух вариантов выше или после полного ручного завершения всей конфигурации. + descEnableLiveSync: Включайте это только после настройки одного из двух + вариантов выше или после полного ручного завершения всей конфигурации. descFetchConfigFromRemote: Получить необходимые настройки с уже настроенного удалённого сервера. descManualSetup: Не рекомендуется, но полезно, если у вас нет Setup URI. - descTestDatabaseConnection: Открыть подключение к базе данных. Если удалённая база данных не найдена и у вас есть право на её создание, база будет создана. - descValidateDatabaseConfig: Проверяет и исправляет любые потенциальные проблемы в конфигурации базы данных. + descTestDatabaseConnection: Открыть подключение к базе данных. Если удалённая + база данных не найдена и у вас есть право на её создание, база будет + создана. + descValidateDatabaseConfig: Проверяет и исправляет любые потенциальные проблемы + в конфигурации базы данных. errAccessForbidden: ❗ Доступ запрещён. errCannotContinueTest: Мы не можем продолжить тест. errCorsCredentials: ❗ cors.credentials неверно @@ -518,22 +953,25 @@ obsidianLiveSyncSettingTab: logCheckingConfigDone: Проверка конфигурации завершена logCheckingConfigFailed: Проверка конфигурации не удалась logCheckingDbConfig: Проверка конфигурации базы данных - logCheckPassphraseFailed: "ОШИБКА: Не удалось проверить пароль с удалённым сервером: db." + logCheckPassphraseFailed: |- + ОШИБКА: Не удалось проверить парольную фразу с удалённым сервером: + ${db}. logConfiguredDisabled: "Настроенный режим синхронизации: ОТКЛЮЧЕН" logConfiguredLiveSync: "Настроенный режим синхронизации: LiveSync" logConfiguredPeriodic: "Настроенный режим синхронизации: Периодический" - logCouchDbConfigFail: "Конфигурация CouchDB: title не удалась" - logCouchDbConfigSet: "Конфигурация CouchDB: title -> Установить key в value" - logCouchDbConfigUpdated: "Конфигурация CouchDB: title успешно обновлена" + logCouchDbConfigFail: "Конфигурация CouchDB: ${title} не удалась" + logCouchDbConfigSet: "Конфигурация CouchDB: ${title} -> Установить ${key} в ${value}" + logCouchDbConfigUpdated: "Конфигурация CouchDB: ${title} успешно обновлена" logDatabaseConnected: База данных подключена logEncryptionNoPassphrase: Вы не можете включить шифрование без парольной фразы logEncryptionNoSupport: Ваше устройство не поддерживает шифрование. logErrorOccurred: Произошла ошибка!! - logEstimatedSize: "Примерный размер: size" + logEstimatedSize: "Примерный размер: ${size}" logPassphraseInvalid: Парольная фраза недействительна, пожалуйста, исправьте. logPassphraseNotCompatible: "ОШИБКА: Парольная фраза несовместима с удалённым сервером!" logRebuildNote: Синхронизация отключена, загрузите и включите снова при желании. logSelectAnyPreset: Выберите любой пресет. + logServerConfigurationCheck: obsidianLiveSyncSettingTab.logServerConfigurationCheck msgAreYouSureProceed: Вы уверены, что хотите продолжить? msgChangesNeedToBeApplied: Изменения нужно применить! msgConfigCheck: --Проверка конфигурации-- @@ -541,7 +979,7 @@ obsidianLiveSyncSettingTab: msgConnectionCheck: --Проверка подключения-- msgConnectionProxyNote: Если у вас проблемы с проверкой подключения, проверьте конфигурацию обратного прокси. - msgCurrentOrigin: "Текущий origin: origin" + msgCurrentOrigin: "Текущий origin: ${origin}" msgDiscardConfirmation: Вы действительно хотите отменить существующие настройки и базы данных? msgDone: --Готово-- msgEnableCors: Установить httpd.enable_cors @@ -556,15 +994,64 @@ obsidianLiveSyncSettingTab: msgNonHTTPSWarning: Не удаётся подключиться к не-HTTPS URI. Обновите конфигурацию. msgNotice: ---Уведомление--- msgObjectStorageWarning: "ПРЕДУПРЕЖДЕНИЕ: Эта функция в разработке." - msgOriginCheck: "Проверка origin: org" - msgRebuildRequired: Требуется перестроение баз данных для применения изменений. + msgOriginCheck: "Проверка origin: ${org}" + msgRebuildRequired: >- + Требуется перестроение баз данных для применения изменений. Выберите способ + применения изменений. + + +
+ + Обозначения + + + | Символ | Значение | + + |: ------ :| ------- | + + | ⇔ | Актуально | + + | ⇄ | Синхронизация для выравнивания | + + | ⇐,⇒ | Передача с перезаписью | + + | ⇠,⇢ | Передача с перезаписью с другой стороны | + + +
+ + + ## ${OPTION_REBUILD_BOTH} + + Коротко: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄 + + Перестроить локальную и удалённую базы данных по файлам, которые уже есть на + этом устройстве. + + Другие устройства будут заблокированы, им потребуется загрузка с удалённого. + + ## ${OPTION_FETCH} + + Коротко: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄 + + Очистить локальную базу данных и построить её заново по данным, загруженным + из удалённой базы данных. + + Этот вариант подходит и тогда, когда вы перестроили удалённую базу данных. + + ## ${OPTION_ONLY_SETTING} + + Сохранить только настройки. **Внимание: это может привести к повреждению + данных**; обычно перестроение базы данных необходимо. msgSetCorsCredentials: Установить cors.credentials msgSetCorsOrigins: Установить cors.origins msgSetMaxDocSize: Установить couchdb.max_document_size msgSetMaxRequestSize: Установить chttpd.max_http_request_size msgSetRequireValidUser: Установить chttpd.require_valid_user = true msgSetRequireValidUserAuth: Установить chttpd_auth.require_valid_user = true - msgSettingModified: Настройка setting была изменена с другого устройства. + msgSettingModified: Настройка «${setting}» была изменена с другого устройства. + Нажмите {HERE}, чтобы перезагрузить настройки. Нажмите в другом месте, чтобы + игнорировать изменения. msgSettingsUnchangeableDuringSync: Эти настройки нельзя изменить во время синхронизации. msgSetWwwAuth: Установить httpd.WWW-Authenticate nameApplySettings: Применить настройки @@ -610,8 +1097,9 @@ obsidianLiveSyncSettingTab: panelPrivacyEncryption: Конфиденциальность и шифрование panelRemoteConfiguration: Удалённая конфигурация panelSetup: Настройка - serverVersion: "Информация о сервере: info" + serverVersion: "Информация о сервере: ${info}" titleActiveRemoteServer: Активный удалённый сервер + titleAdvancedSettings: Дополнительные настройки titleAppearance: Внешний вид titleConflictResolution: Разрешение конфликтов titleCouchDB: Сервер CouchDB @@ -619,11 +1107,16 @@ obsidianLiveSyncSettingTab: titleEncryptionNotEnabled: Шифрование не включено titleEncryptionPassphraseInvalid: Парольная фраза шифрования недействительна titleExtraFeatures: Включить дополнительные и расширенные функции + titleExtraFeaturesGroup: Дополнительные возможности + titleExtraMenus: Дополнительные меню titleFetchConfig: Загрузить конфигурацию titleFetchConfigFromRemote: Загрузить конфигурацию с удалённого сервера titleFetchSettings: Загрузить настройки + titleHelpAndInformation: Справка и сведения + titleHelpAndTroubleshooting: Справка и решение проблем titleHiddenFiles: Скрытые файлы titleLogging: Логирование + titleMaintenanceAndRecovery: Обслуживание и восстановление titleMinioS3R2: MinIO, S3, R2 titleNotification: Уведомления titleOnlineTips: Онлайн советы @@ -633,23 +1126,41 @@ obsidianLiveSyncSettingTab: titleRemoteServer: Удалённый сервер titleReset: Сброс titleSetupOtherDevices: Для настройки других устройств + titleSynchronisation: Синхронизация titleSynchronizationMethod: Метод синхронизации titleSynchronizationPreset: Пресет синхронизации titleSyncSettings: Настройки синхронизации titleSyncSettingsViaMarkdown: Синхронизация настроек через Markdown titleUpdateThinning: Оптимизация обновлений - warnCorsOriginUnmatched: ⚠ CORS Origin не совпадает from->to + warnCorsOriginUnmatched: ⚠ CORS Origin не совпадает ${from}->${to} warnNoAdmin: ⚠ У вас нет прав администратора. +Of course, we can back up the data before proceeding.: Конечно, перед этим можно сделать резервную копию данных. +Off: Выкл Ok: ОК Old Algorithm: Старый алгоритм +Older (${diff}): Старше (${diff}) Older fallback (Slow, W/O WebAssembly): Старый вариант fallback (медленный, без WebAssembly) +On: Вкл +On the source device, from the command palette, run the 'Show settings as a QR code' command.: + На исходном устройстве в палитре команд выполните команду «Показать настройки + как QR-код». +On the source device, open Obsidian.: На исходном устройстве откройте Obsidian. +On this device, please keep this Vault open.: На этом устройстве не закрывайте хранилище. +On this device, switch to the camera app or use a QR code scanner to scan the displayed QR code.: + На этом устройстве откройте камеру или сканер QR-кодов и считайте показанный + QR-код. Open: Открыть +Open connection: Открыть соединение +Open P2P Setup...: Открыть настройку P2P... Open the dialog: Открыть диалог +Other files: Другие файлы Overwrite: Перезаписать Overwrite patterns: Шаблоны перезаписи Overwrite remote: Перезаписать удалённое хранилище Overwrite remote with local DB and passphrase.: Перезаписать удалённое хранилище локальной БД и парольной фразой. Overwrite Server Data with This Device's Files: Перезаписать данные сервера файлами с этого устройства +P2P Configuration: Настройки P2P +P2P Status: Состояние P2P P2P: AskPassphraseForDecrypt: Удалённое устройство предоставило конфигурацию. Введите пароль для расшифровки. @@ -670,24 +1181,31 @@ P2P: P2PReplication: P2P Репликация PaneTitle: long_p2p_sync ReplicatorInstanceMissing: P2P Sync репликатор не найден, возможно, не настроен. - SeemsOffline: Устройство name офлайн, пропущено. + SeemsOffline: Устройство ${name} офлайн, пропущено. SyncAlreadyRunning: P2P Sync уже запущен. SyncCompleted: P2P Sync завершён. - SyncStartedWith: P2P Sync с name начат. + SyncStartedWith: P2P Sync с ${name} начат. paneMaintenance: markDeviceResolvedAfterBackup: Пометить устройство как обработанное после резервного копирования remoteLockedAndDeviceNotAccepted: Удалённая база данных заблокирована, и это устройство ещё не одобрено. remoteLockedResolvedDevice: Удалённая база данных заблокирована, но это устройство уже одобрено. unlockDatabaseReady: Разблокировать базу данных Passphrase: Парольная фраза +Passphrase is required.: Нужна парольная фраза. Passphrase of sensitive configuration items: Парольная фраза для конфиденциальных настроек password: пароль Password: Пароль Paste a connection string: Вставить строку подключения +Paste the Setup URI generated from one of your active devices.: Вставьте Setup URI, созданный на одном из ваших активных устройств. +Path: Путь Path Obfuscation: Обфускация путей Patterns to match files for overwriting instead of merging: Шаблоны для файлов, которые нужно перезаписывать вместо объединения Patterns to match files for syncing: Шаблоны для файлов, которые нужно синхронизировать +"Peer ID:": "Peer ID:" +Peer to Peer Replicator: P2P репликатор +Peer-to-Peer only: Только Peer-to-Peer Peer-to-Peer Synchronisation: Одноранговая синхронизация +Peers: Устройства Per-file-saved customization sync: Синхронизация настроек для каждого файла Perform: Выполнить Perform cleanup: Выполнить очистку @@ -696,21 +1214,67 @@ Perform Garbage Collection to remove unused chunks and reduce database size.: Выполняет сборку мусора, чтобы удалить неиспользуемые чанки и уменьшить размер базы данных. Periodic Sync interval: Интервал периодической синхронизации +PERMANENT: НАВСЕГДА Pick a file to resolve conflict: Выбрать файл для разрешения конфликта +Pick a file to show history: Выберите файл, чтобы посмотреть историю +Please be aware that the End-to-End Encryption passphrase is not validated until the synchronisation process actually commences. This is a security measure designed to protect your data.: + "Учтите: парольная фраза сквозного шифрования проверяется только при запуске + синхронизации. Так сделано для защиты данных." +Please configure your end-to-end encryption settings.: Задайте настройки сквозного шифрования. +Please disable 'Read chunks online' in settings to use Garbage Collection.: + Чтобы использовать Garbage Collection, отключите в настройках «Read chunks + online». +Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.: + Чтобы использовать Garbage Collection, включите в настройках «Compute + revisions for chunks». +Please enter the CouchDB server information below.: Укажите данные сервера CouchDB ниже. +Please enter the details required to connect to your S3/MinIO/R2 compatible object storage service.: + Укажите данные для подключения к объектному хранилищу, совместимому с + S3/MinIO/R2. +Please enter the Peer-to-Peer Synchronisation information below.: Укажите данные P2P-синхронизации ниже. +Please enter the Setup URI that was generated during server installation or on another device, along with the vault passphrase.: + Введите Setup URI, полученный при установке сервера или на другом устройстве, + и парольную фразу Vault. +Please follow the steps below to import settings from your existing device.: Выполните шаги ниже, чтобы перенести настройки с уже настроенного устройства. +PLEASE NOTE: ОБРАТИТЕ ВНИМАНИЕ +Please select 'Cancel' explicitly to cancel this operation.: Чтобы отменить эту операцию, явно выберите «Отмена». +Please select a method to import the settings from another device.: Выберите способ импорта настроек с другого устройства. +Please select an active P2P remote configuration to change P2P sync targets.: + Выберите активную настройку удалённой базы P2P, чтобы сменить цели + P2P-синхронизации. +Please select an option to proceed: Чтобы продолжить, выберите вариант +Please select the button below to restart and proceed to the data fetching confirmation.: + Нажмите кнопку ниже, чтобы перезапустить и перейти к подтверждению загрузки + данных. +Please select the button below to restart and proceed to the final confirmation.: Нажмите кнопку ниже, чтобы перезапустить и перейти к последнему подтверждению. +Please select the type of server to which you are connecting.: Выберите тип сервера, к которому вы подключаетесь. +Please select your situation.: Выберите свой случай. Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.: Укажите имя устройства для идентификации этого устройства. Имя должно быть уникальным среди ваших устройств. Пока оно не задано, мы не можем включить эту функцию. Please set this device name: Укажите имя этого устройства +Please understand that this is intended behaviour.: Так и задумано. +Plug-in version: Версия плагина +Plugins: Плагины Prepare the 'report' to create an issue: Подготовить «отчёт» для создания Issue Presets: Пресеты +Prevent fetching configuration from server: Не забирать настройки с сервера +Proceed: Продолжить +Proceed Garbage Collection: Продолжить Garbage Collection +Proceed to the next step.: Перейти к следующему шагу. +Proceed with Setup URI: Продолжить с Setup URI +Proceeding with Garbage Collection, ignoring missing nodes.: Продолжаем Garbage Collection, игнорируя отсутствующие узлы. +Proceeding with Garbage Collection.: Запускаем Garbage Collection. Process small files in the foreground: Обрабатывать маленькие файлы в основном потоке +Progress: Прогресс Property Encryption: Шифрование свойств PureJS fallback (Fast, W/O WebAssembly): Вариант PureJS (быстрый, без WebAssembly) Purge all download/upload cache.: Очистить весь кэш загрузки/выгрузки. Purge all journal counter: Очистить все счётчики журнала Rebuild local and remote database with local files.: Перестроить локальную и удалённую базы данных на основе локальных файлов. Rebuilding Operations (Remote Only): Операции перестроения (только удалённое хранилище) +Recovery and Repair: Восстановление и исправление Recreate all: Пересоздать всё Recreate missing chunks for all files: Пересоздать отсутствующие чанки для всех файлов RedFlag: @@ -727,16 +1291,29 @@ RedFlag: Fetch: Да, загрузить и применить удалённые настройки Message: Вы хотите загрузить и применить удалённые настройки? Title: Загрузить удалённую конфигурацию +Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.: + "Освобождает место: удаляет все ревизии, кроме последней. Нужно столько же + свободного места на сервере и на устройстве." Reducing the frequency with which on-disk changes are reflected into the DB: Уменьшение частоты отражения изменений с диска в БД +Refresh: Обновить Region: Регион +Relay settings: Настройки ретранслятора +Reload: Перезагрузить Remediation: Исправление Remediation Setting Changed: Настройки исправления изменены Remote Database Tweak (In sunset): Настройки удалённой базы данных (устаревает) Remote Databases: Удалённые базы данных Remote name: Имя удалённого хранилища +Remote only: Только в удалённой базе Remote server type: Тип удалённого сервера Remote Type: Удалённый тип Rename: Переименовать +Replicate now: Реплицировать сейчас +Replicating: + _value: Идёт репликация + "": + "": + "": Идёт репликация... Replicator: Dialogue: Locked: @@ -766,6 +1343,7 @@ Resend all chunks to the remote.: Повторно отправить все ч Reset: Сброс Reset all: Сбросить всё Reset all journal counter: Сбросить все счётчики журнала +Reset and Resume Synchronisation: Сбросить и продолжить синхронизацию Reset journal received history: Сбросить историю полученных записей журнала Reset journal sent history: Сбросить историю отправленных записей журнала Reset notification threshold and check the remote database usage: Сбросить порог уведомления и проверить использование удалённой базы данных @@ -780,33 +1358,137 @@ Resolve All conflicted files by the newer one: Разрешить все кон "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": "Разрешает все конфликтующие файлы в пользу более новой версии. Внимание: старая версия будет перезаписана и её нельзя будет восстановить." +Restart and Fetch Data: Перезапустить и забрать данные с сервера +Restart and Initialise Server: Перезапустить и инициализировать сервер Restart Now: Перезапустить сейчас +Restarting Obsidian is strongly recommended. Until restart, some changes may not take effect, and display may be inconsistent. Are you sure to restart now?: + Лучше перезапустить Obsidian. До перезапуска часть изменений не применится, а + вид может быть неверным. Перезапустить сейчас? Restore or reconstruct local database from remote.: Восстановить или перестроить локальную базу данных из удалённой. +Rev: Ревизия +Revert changes: Откатить изменения +Revoke: Отозвать +Room ID: ID комнаты +"Room ID suffix:": "Суффикс ID комнаты:" Run Doctor: Запустить диагностику +S3/MinIO/R2 Configuration: Настройки S3/MinIO/R2 +S3/MinIO/R2 Object Storage: Объектное хранилище S3/MinIO/R2 +Same: Совпадает +Same or local only: Совпадает или есть только локально +Save and Apply: Сохранить и применить Save settings to a markdown file.: Сохранить настройки в файл markdown. Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.: - Save settings to a markdown file. You will be notified when new settings - arrive. You can set different files by the platform. + Сохранить настройки в файл markdown. Вы получите уведомление, когда придут + новые настройки. Для разных платформ можно задать разные файлы. +Save without connecting: Сохранить без проверки связи Saving will be performed forcefully after this number of seconds.: Сохранение будет принудительно выполнено после этого количества секунд. +Saving without a successful connection test keeps this profile, but automatic synchronisation may fail until the connection is corrected.: + Профиль сохранится и без успешной проверки связи, но автоматическая + синхронизация может не работать, пока подключение не исправлено. +Scan a QR Code (Recommended for mobile): Сканировать QR-код (рекомендуется для мобильных устройств) +Scan changes: Проверить изменения Scan changes on customization sync: Сканировать изменения при синхронизации настроек Scan customization automatically: Сканировать настройки автоматически Scan customization before replicating.: Сканировать настройки перед репликацией. Scan customization every 1 minute.: Сканировать настройки каждую минуту. Scan customization periodically: Сканировать настройки периодически +Scan for Broken files: Найти повреждённые файлы Scan for hidden files before replication: Сканировать скрытые файлы перед репликацией Scan hidden files periodically: Сканировать скрытые файлы периодически +Scan QR Code: Сканировать QR-код +Scan the QR code displayed on an active device using this device's camera.: + Отсканируйте QR-код, показанный на активном устройстве, с помощью камеры этого + устройства. Schedule and Restart: Запланировать и перезапустить +Scram Switches: Аварийные выключатели Scram!: Экстренные меры Seconds, 0 to disable: Секунд, 0 для отключения Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.: Секунды. Сохранение в локальную базу данных будет отложено. +Secret Access Key: Secret Access Key Secret Key: Секретный ключ +Select active P2P remote: Выберите активную удалённую базу P2P +Select All Shiny: Выбрать все новые +Select Flagged Shiny: Выбрать отмеченные новые +Select P2P remote...: Выберите удалённую базу P2P... Select the database adapter to use.: Выберите используемый адаптер базы данных. +Selecting this option will result in the current data on this device being used to initialise the server. Any existing data on the server will be completely overwritten.: + Сервер получит данные с этого устройства. Всё, что уже есть на сервере, будет + полностью перезаписано. +Selecting this option will result in this device joining the existing server. You need to fetching the existing synchronisation data from the server to this device.: + Это устройство подключится к уже настроенному серверу. Данные синхронизации + надо забрать с сервера на это устройство. +Selective: Выборочно Send: Отправить Send chunks: Отправить чанки +SENDING: ОТПРАВКА Server URI: URI сервера +SESSION: СЕАНС Setting: GenerateKeyPair: - Desc: Мы сгенерировали пару ключей! + Desc: >+ + Мы сгенерировали пару ключей! + + + Примечание: эта пара ключей больше никогда не будет показана. Сохраните её + в надёжном месте. Если вы её потеряете, вам нужно будет сгенерировать + новую пару ключей. + + Примечание 2: открытый ключ в формате spki, а закрытый ключ — в формате + pkcs8. Для удобства переносы строк в открытом ключе заменены на `\n`. + + Примечание 3: открытый ключ настраивается в удалённой базе данных, а + закрытый ключ — на локальных устройствах. + + + >[!FOR YOUR EYES ONLY]- + + >
+ + > + + > ### Открытый ключ + + > ``` + + ${public_key} + + > ``` + + > + + > ### Закрытый ключ + + > ``` + + ${private_key} + + > ``` + + > + + >
+ + + >[!Both for copying]- + + > + + >
+ + > + + > ``` + + ${public_key} + + ${private_key} + + > ``` + + > + + >
+ Title: Новая пара ключей сгенерирована! TroubleShooting: _value: Устранение неполадок @@ -820,6 +1502,10 @@ SettingTab: Message: AskRebuild: Ваши изменения требуют загрузки из удалённой базы данных. Хотите продолжить? +"Setup Complete: Preparing to Fetch Synchronisation Data": "Настройка завершена: готовимся забрать данные синхронизации" +"Setup Complete: Preparing to Initialise Server": "Настройка завершена: готовимся инициализировать сервер" +Setup URI dialog cancelled.: Диалог Setup URI был отменён. +Setup-URI: Setup-URI Setup: Apply: Buttons: @@ -829,7 +1515,7 @@ Setup: Cancel: Отменить и отменить OnlyApply: Только применить Message: Новая конфигурация готова. Есть несколько способов применить её. - Title: Применить новую конфигурацию из method + Title: Применить новую конфигурацию из ${method} WarningRebuildRecommended: "ПРИМЕЧАНИЕ: после настройки изменений определено, что требуется перестроение." Doctor: @@ -846,118 +1532,128 @@ Setup: Message: Если мы уже синхронизировались с другим устройством, удалённая база данных хранит подходящие значения конфигурации. Title: Загрузить конфигурацию с удалённой базы данных? - QRCode: Мы сгенерировали QR-код для передачи настроек. Отсканируйте QR-код телефоном. + QRCode: >- + Мы сгенерировали QR-код для передачи настроек. Отсканируйте QR-код телефоном + или другим устройством. + + Внимание: QR-код не зашифрован, открывайте его осторожно. + + + >[!FOR YOUR EYES ONLY]- + + >
${qr_image}
+ RemoteE2EE: + AdvancedTitle: Дополнительно + AlgorithmWarning: Изменение алгоритма шифрования лишит доступа к данным, которые + ранее были зашифрованы другим алгоритмом. Убедитесь, что все ваши + устройства настроены на использование одного и того же алгоритма, чтобы + сохранить доступ к данным. + ButtonCancel: Отмена + ButtonProceed: Продолжить + DefaultAlgorithmDesc: В большинстве случаев следует оставить алгоритм по + умолчанию (${algorithm}). Этот параметр нужен только в том случае, если у + вас уже есть Vault, зашифрованный в другом формате. + Guidance: Пожалуйста, настройте параметры сквозного шифрования. + LabelEncrypt: Сквозное шифрование + LabelEncryptionAlgorithm: Алгоритм шифрования + LabelObfuscateProperties: Обфусцировать свойства + MultiDestinationWarning: Этот параметр должен быть одинаковым даже при + подключении к нескольким направлениям синхронизации. + ObfuscatePropertiesDesc: Обфускация свойств (например, пути к файлу, размера, + дат создания и изменения) добавляет дополнительный уровень защиты, + затрудняя определение структуры и названий ваших файлов и папок на + удалённом сервере. Это помогает защитить вашу конфиденциальность и + усложняет для посторонних вывод информации о ваших данных. + PassphraseValidationLine1: "Обратите внимание: парольная фраза для сквозного + шифрования не проверяется до фактического начала процесса синхронизации. + Это сделано в целях безопасности ваших данных." + PassphraseValidationLine2: Поэтому при ручной настройке информации о сервере + требуется предельная осторожность. Если будет введена неверная парольная + фраза, данные на сервере будут повреждены. Пожалуйста, учтите, что это + ожидаемое поведение. + PlaceholderPassphrase: Введите парольную фразу + StronglyRecommendedLine1: При включении сквозного шифрования ваши данные + шифруются на устройстве до отправки на удалённый сервер. Это означает, что + даже если кто-то получит доступ к серверу, он не сможет прочитать ваши + данные без парольной фразы. Обязательно запомните парольную фразу, так как + она потребуется для расшифровки данных на других устройствах. + StronglyRecommendedLine2: "Также обратите внимание: если вы используете + синхронизацию Peer-to-Peer, эта конфигурация будет использована, когда вы + позже переключитесь на другие методы и подключитесь к удалённому серверу." + StronglyRecommendedTitle: Настоятельно рекомендуется + Title: Сквозное шифрование + ScanQRCode: + ButtonClose: Закрыть это окно + Guidance: Чтобы импортировать настройки с существующего устройства, выполните + следующие шаги. + Step1: На этом устройстве оставьте данный Vault открытым. + Step2: На исходном устройстве откройте Obsidian. + Step3: На исходном устройстве в палитре команд выполните команду «Показать + настройки как QR-код». + Step4: На этом устройстве откройте приложение камеры или используйте сканер + QR-кодов, чтобы считать показанный QR-код. + Title: Сканировать QR-код ShowQRCode: _value: Показать QR код Desc: Показать QR код для передачи настроек. - RemoteE2EE: - Title: Сквозное шифрование - Guidance: Пожалуйста, настройте параметры сквозного шифрования. - LabelEncrypt: Сквозное шифрование - PlaceholderPassphrase: Введите парольную фразу - StronglyRecommendedTitle: Настоятельно рекомендуется - StronglyRecommendedLine1: При включении сквозного шифрования ваши данные шифруются на устройстве до отправки на удалённый сервер. Это означает, что даже если кто-то получит доступ к серверу, он не сможет прочитать ваши данные без парольной фразы. Обязательно запомните парольную фразу, так как она потребуется для расшифровки данных на других устройствах. - StronglyRecommendedLine2: >- - Также обратите внимание: если вы используете синхронизацию Peer-to-Peer, эта конфигурация будет использована, когда вы позже переключитесь на другие методы и подключитесь к удалённому серверу. - MultiDestinationWarning: Этот параметр должен быть одинаковым даже при подключении к нескольким направлениям синхронизации. - LabelObfuscateProperties: Обфусцировать свойства - ObfuscatePropertiesDesc: Обфускация свойств (например, пути к файлу, размера, дат создания и изменения) добавляет дополнительный уровень защиты, затрудняя определение структуры и названий ваших файлов и папок на удалённом сервере. Это помогает защитить вашу конфиденциальность и усложняет для посторонних вывод информации о ваших данных. - AdvancedTitle: Дополнительно - LabelEncryptionAlgorithm: Алгоритм шифрования - DefaultAlgorithmDesc: В большинстве случаев следует оставить алгоритм по умолчанию (${algorithm}). Этот параметр нужен только в том случае, если у вас уже есть Vault, зашифрованный в другом формате. - AlgorithmWarning: Изменение алгоритма шифрования лишит доступа к данным, которые ранее были зашифрованы другим алгоритмом. Убедитесь, что все ваши устройства настроены на использование одного и того же алгоритма, чтобы сохранить доступ к данным. - PassphraseValidationLine1: >- - Обратите внимание: парольная фраза для сквозного шифрования не проверяется до фактического начала процесса синхронизации. Это сделано в целях безопасности ваших данных. - PassphraseValidationLine2: Поэтому при ручной настройке информации о сервере требуется предельная осторожность. Если будет введена неверная парольная фраза, данные на сервере будут повреждены. Пожалуйста, учтите, что это ожидаемое поведение. - ButtonProceed: Продолжить - ButtonCancel: Отмена UseSetupURI: - Title: Ввести Setup URI - GuidanceLine1: Введите Setup URI, созданный во время установки сервера или на другом устройстве, а также парольную фразу Vault. - GuidanceLine2: Новый Setup URI можно создать, выполнив в палитре команд команду «Скопировать настройки как новый Setup URI». - LabelSetupURI: Setup URI - ValidInfo: Setup URI корректен и готов к использованию. + ButtonCancel: Отмена + ButtonProceed: Проверить настройки и продолжить + ErrorFailedToParse: Не удалось обработать Setup URI. Проверьте URI и парольную фразу. + ErrorPassphraseRequired: Пожалуйста, введите парольную фразу Vault. + GuidanceLine1: Введите Setup URI, созданный во время установки сервера или на + другом устройстве, а также парольную фразу Vault. + GuidanceLine2: Новый Setup URI можно создать, выполнив в палитре команд команду + «Скопировать настройки как новый Setup URI». InvalidInfo: Setup URI некорректен. Проверьте его и попробуйте снова. LabelPassphrase: Парольная фраза Vault + LabelSetupURI: Setup URI PlaceholderPassphrase: Введите парольную фразу Vault - ErrorPassphraseRequired: Пожалуйста, введите парольную фразу Vault. - ErrorFailedToParse: Не удалось обработать Setup URI. Проверьте URI и парольную фразу. - ButtonProceed: Проверить настройки и продолжить - ButtonCancel: Отмена - ScanQRCode: - Title: Сканировать QR-код - Guidance: Чтобы импортировать настройки с существующего устройства, выполните следующие шаги. - Step1: На этом устройстве оставьте данный Vault открытым. - Step2: На исходном устройстве откройте Obsidian. - Step3: На исходном устройстве в палитре команд выполните команду «Показать настройки как QR-код». - Step4: На этом устройстве откройте приложение камеры или используйте сканер QR-кодов, чтобы считать показанный QR-код. - ButtonClose: Закрыть это окно -"Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": Чтобы использовать Garbage Collection, включите в настройках «Compute revisions for chunks». -"Please disable 'Read chunks online' in settings to use Garbage Collection.": Чтобы использовать Garbage Collection, отключите в настройках «Read chunks online». -"Setup URI dialog cancelled.": Диалог Setup URI был отменён. -"Please select 'Cancel' explicitly to cancel this operation.": Чтобы отменить эту операцию, явно выберите «Отмена». -"Failed to connect to remote for compaction.": Не удалось подключиться к удалённой базе данных для компакции. -"Failed to connect to remote for compaction. ${reason}": Не удалось подключиться к удалённой базе данных для компакции. ${reason} -"Compaction in progress on remote database...": Выполняется компакция удалённой базы данных... -"Compaction on remote database timed out.": Время ожидания компакции удалённой базы данных истекло. -"Compaction on remote database completed successfully.": Компакция удалённой базы данных успешно завершена. -"Compaction on remote database failed.": Компакция удалённой базы данных завершилась ошибкой. -"Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": Не удалось запустить одноразовую репликацию перед Garbage Collection. Garbage Collection отменена. -"Cancel Garbage Collection": Отменить Garbage Collection -"No connected device information found. Cancelling Garbage Collection.": Не найдена информация о подключённых устройствах. Garbage Collection отменяется. -"The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": |- - Для следующих принятых узлов отсутствует информация об узле: - - ${missingNodes} - - Это означает, что они давно не подключались или остались на старой версии. - По возможности рекомендуется сначала обновить все устройства. Если у вас есть устройства, которые больше не используются, вы можете очистить список всех принятых узлов, один раз заблокировав удалённую базу. -"Ignore and Proceed": Игнорировать и продолжить -"Node Information Missing": Отсутствует информация об узле -"Garbage Collection cancelled by user.": Пользователь отменил Garbage Collection. -"Proceeding with Garbage Collection, ignoring missing nodes.": Продолжаем Garbage Collection, игнорируя отсутствующие узлы. -"Proceed Garbage Collection": Продолжить Garbage Collection -"> [!INFO]- The connected devices have been detected as follows:\n${devices}": |- - > [!INFO]- Обнаружены следующие подключённые устройства: - ${devices} -"Device": Устройство -"Node ID": ID узла -"Obsidian version": Версия Obsidian -"Plug-in version": Версия плагина -"Progress": Прогресс -"Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": |- - У некоторых устройств различаются значения прогресса (макс.: ${maxProgress}, мин.: ${minProgress}). - Это может означать, что некоторые устройства ещё не завершили синхронизацию, что может привести к конфликтам. Настоятельно рекомендуется перед продолжением убедиться, что все устройства синхронизированы. -"All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": У всех устройств одинаковое значение прогресса (${progress}). Похоже, ваши устройства синхронизированы, и можно продолжать Garbage Collection. -"Garbage Collection Confirmation": Подтверждение Garbage Collection -"Proceeding with Garbage Collection.": Запускаем Garbage Collection. -"Garbage Collection: Scanned ${scanned} / ~${docCount}": |- - Garbage Collection: просканировано ${scanned} / ~${docCount} -"Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": |- - Garbage Collection: сканирование завершено. Всего чанков: ${totalChunks}, используемых чанков: ${usedChunks} -"Garbage Collection: Found ${unusedChunks} unused chunks to delete.": |- - Garbage Collection: найдено ${unusedChunks} неиспользуемых чанков для удаления. -"Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": |- - Garbage Collection завершена. Удалено чанков: ${deletedChunks} / ${totalChunks}. Затраченное время: ${seconds} сек. -"Failed to start replication after Garbage Collection.": Не удалось запустить репликацию после Garbage Collection. + Title: Ввести Setup URI + ValidInfo: Setup URI корректен и готов к использованию. Should we keep folders that don't have any files inside?: Сохранять папки без файлов? Should we only check for conflicts when a file is opened?: Проверять конфликты только при открытии файла? Should we prompt you about conflicting files when a file is opened?: Спрашивать о конфликтующих файлах при открытии файла? Should we prompt you for every single merge, even if we can safely merge automatcially?: Спрашивать о каждом слиянии, даже если мы можем безопасно слить автоматически? Show full banner: Показывать полный баннер +Show history: Показать историю +Show icon only: Показывать только значок Show only notifications: Показывать только уведомления +Show password: Показать пароль Show status as icons only: Показывать статус только иконками Show status icon instead of file warnings banner: Показывать иконку статуса вместо предупреждения о файлах Show status inside the editor: Показывать статус внутри редактора Show status on the status bar: Показывать статус в строке состояния Show verbose log. Please enable if you report an issue.: Показывать подробный лог. Пожалуйста, включите при сообщении о проблеме. +Signaling Server Connection: Подключение к сигнальному серверу +Signalling Status: Состояние сигнального соединения +Skip and close: Пропустить и закрыть +Snippets: Сниппеты +"Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": >- + У некоторых устройств различаются значения прогресса (макс.: ${maxProgress}, + мин.: ${minProgress}). + + Это может означать, что некоторые устройства ещё не завершили синхронизацию, + что может привести к конфликтам. Настоятельно рекомендуется перед продолжением + убедиться, что все устройства синхронизированы. +Start Broadcasting: Начать вещание +Start change-broadcasting on Connect: Начинать вещание изменений при подключении +Start Sync & Close: Запустить синхронизацию и закрыть Starts synchronisation when a file is saved.: Запускать синхронизацию при сохранении файла. +Stat: Статистика +Stats: Статистика +Stop ⚡: Стоп ⚡ +Stop Broadcasting: Остановить вещание Stop reflecting database changes to storage files.: Остановить отражение изменений базы данных в файлы хранилища. Stop watching for file changes.: Остановить отслеживание изменений файлов. +Storage -> Database: Файлы -> База данных +Strongly Recommended: Настоятельно рекомендуем Suppress notification of hidden files change: Подавлять уведомления об изменении скрытых файлов Suspend database reflecting: Приостановить отражение базы данных Suspend file watching: Приостановить отслеживание файлов Switch to IDB: Переключиться на IDB Switch to IndexedDB: Переключиться на IndexedDB +Sync: Синхронизация Sync after merging file: Синхронизировать после слияния файла Sync automatically after merging files: Синхронизировать автоматически после слияния файлов Sync Mode: Режим синхронизации @@ -965,38 +1661,138 @@ Sync on Editor Save: Синхронизация при сохранении в Sync on File Open: Синхронизация при открытии файла Sync on Save: Синхронизация при сохранении Sync on Startup: Синхронизация при запуске +Sync once: Синхронизировать один раз +Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.: + Синхронизация с использованием файлов журнала. Необходимо заранее настроить + объектное хранилище, совместимое с S3/MinIO/R2. Synchronising files: Синхронизируемые файлы -Syncing: Синхронизация +Syncing: + _value: Синхронизация + "": + "": + "": Синхронизация... Target patterns: Целевые шаблоны +Test connection and save: Проверить подключение и сохранить +Test Settings and Continue: Проверить настройки и продолжить Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.: Только для тестирования - разрешать конфликты файлов синхронизацией новых копий. +The connection test cannot add a signalling relay while P2P is active. Use the active relay settings, or disconnect P2P before testing.: + Пока P2P активен, проверка связи не может добавить сигнальный ретранслятор. + Используйте текущие настройки ретранслятора или отключите P2P перед проверкой. +The connection to the server has been configured successfully. As the next step,: Подключение к серверу настроено. Следующим шагом The delay for consecutive on-demand fetches: Задержка для последовательных запросов по требованию +The files in this Vault are almost identical to the server's.: Файлы в этом хранилище почти совпадают с файлами на сервере. +"The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": >- + Для следующих принятых узлов отсутствует информация об узле: + + - ${missingNodes} + + + Это означает, что они давно не подключались или остались на старой версии. + + По возможности рекомендуется сначала обновить все устройства. Если у вас есть + устройства, которые больше не используются, вы можете очистить список всех + принятых узлов, один раз заблокировав удалённую базу. +The Group ID and passphrase are used to identify your group of devices. Make sure to use the same Group ID and passphrase on all devices you want to synchronise.: + ID группы и парольная фраза определяют вашу группу устройств. Задайте + одинаковые ID группы и парольную фразу на всех устройствах, которые хотите + синхронизировать. The Hash algorithm for chunk IDs: Хэш-алгоритм для ID чанков +The IndexedDB adapter often offers superior performance in certain scenarios, but it has been found to cause memory leaks when used with LiveSync mode. When using LiveSync mode, please use IDB adapter instead.: + Адаптер IndexedDB в некоторых случаях работает заметно быстрее, но в режиме + LiveSync он вызывает утечки памяти. В режиме LiveSync используйте адаптер IDB. +the latest synchronisation data will be downloaded from the server to this device.: на это устройство будут загружены свежие данные синхронизации с сервера. +the local database, that is to say the synchronisation information, must be reconstituted.: локальную базу, то есть данные синхронизации, надо собрать заново. The maximum duration for which chunks can be incubated within the document.: Максимальная продолжительность инкубации чанков в документе. The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.: - The maximum duration for which chunks can be incubated within the document. - Chunks exceeding this period will graduate to independent chunks. + Максимальная продолжительность инкубации чанков в документе. Чанки, + превысившие этот срок, перейдут в независимые чанки. The maximum number of chunks that can be incubated within the document.: Максимальное количество инкубируемых чанков в документе. The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.: - The maximum number of chunks that can be incubated within the document. Chunks - exceeding this number will immediately graduate to independent chunks. + Максимальное количество инкубируемых чанков в документе. Чанки сверх этого + количества сразу перейдут в независимые чанки. The maximum total size of chunks that can be incubated within the document.: Максимальный общий размер инкубируемых чанков в документе. The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.: - The maximum total size of chunks that can be incubated within the document. - Chunks exceeding this size will immediately graduate to independent chunks. + Максимальный общий размер инкубируемых чанков в документе. Чанки, превысившие + этот размер, сразу перейдут в независимые чанки. The minimum interval for automatic synchronisation on event.: Минимальный интервал автоматической синхронизации по событию. +The remote is already set up, and the configuration is compatible (or got compatible by this operation).: + Удалённая база уже настроена, и настройки совместимы (или стали совместимы + после этой операции). +The Setup-URI does not appear to be valid. Please check that you have copied it correctly.: Setup-URI выглядит неверным. Проверьте, что скопировали его правильно. +The Setup-URI is valid and ready to use.: Setup-URI верный и готов к работе. +the single, authoritative master copy: единственный достоверный источник +the synchronisation data on the server will be built based on the current data on this device.: + данные синхронизации на сервере будут построены по текущим данным этого + устройства. +Themes: Темы +There is a way to resolve this on other devices.: На других устройствах это можно решить. +There may be differences between the files in this Vault and the server.: Файлы в этом хранилище могут отличаться от файлов на сервере. +Therefore, we ask that you exercise extreme caution when configuring server information manually. If an incorrect passphrase is entered, the data on the server will become corrupted.: + Поэтому вводите данные сервера вручную очень внимательно. Если ввести неверную + парольную фразу, данные на сервере испортятся. +This can isolate your connections between devices. Use the same Room ID for the same devices.: + Так можно разделить подключения между устройствами. Для одних и тех же + устройств задайте одинаковый ID комнаты. +This device: Это устройство +This device name: Имя этого устройства +This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.: + Эта функция обеспечивает прямую синхронизацию между устройствами. Сервер не + требуется, но для синхронизации оба устройства должны быть одновременно в + сети, а некоторые функции могут быть ограничены. Подключение к Интернету нужно + только для сигнализации (обнаружения пиров), а не для передачи данных. +This first setup has several short steps because it confirms encryption, the connection method, and which device provides the initial data. Once it is complete, additional devices can reuse a Setup URI.: + "Первая настройка идёт в несколько коротких шагов: надо подтвердить + шифрование, способ подключения и то, какое устройство даёт первые данные. + После этого другие устройства можно настроить через Setup URI." +This is an advanced option for users who do not have a URI or who wish to configure detailed settings.: + Это расширенный вариант для пользователей, у которых нет URI или которые хотят + вручную задать подробные параметры. +This is an extremely powerful operation. We strongly recommend that you copy your Vault folder to a safe location.: + Это очень мощная операция. Настоятельно рекомендуем скопировать папку + хранилища в безопасное место. +This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.: + Это наиболее подходящий для данной архитектуры способ синхронизации. Доступны + все функции. Необходимо заранее развернуть экземпляр CouchDB. +This optional check uses Obsidian's internal request API and sends the credentials above to the CouchDB server. Use it only with a server you trust; administrator access may be required.: + Эта необязательная проверка использует внутренний API запросов Obsidian и + отправляет указанные выше учётные данные на сервер CouchDB. Пользуйтесь ей + только с доверенным сервером; могут понадобиться права администратора. This passphrase will not be copied to another device. It will be set to until you configure it again.: Эта парольная фраза не будет скопирована на другое устройство. This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.: - This passphrase will not be copied to another device. It will be set to - `Default` until you configure it again. + Эта парольная фраза не будет скопирована на другое устройство. Ей будет + присвоено значение `Default`, пока вы не настроите её снова. +This password is used to encrypt the connection. Use something long enough.: Этот пароль шифрует подключение. Возьмите достаточно длинный. +This procedure will first delete all existing synchronisation data from the server. Following this, the server data will be completely rebuilt, using the current state of your Vault on this device (including its local database) as: + Сначала все данные синхронизации на сервере будут удалены. Затем данные на + сервере соберутся заново, а за основу возьмётся текущее состояние хранилища на + этом устройстве (вместе с локальной базой) как +This setting must be the same even when connecting to multiple synchronisation destinations.: + Этот параметр должен совпадать даже при подключении к нескольким целям + синхронизации. +This Vault is empty, or contains only new files that are not on the server.: Это хранилище пустое или содержит только новые файлы, которых нет на сервере. +This will rebuild the local database on this device using the most recent data from the server. This action is designed to resolve synchronisation inconsistencies and restore correct functionality.: + Локальная база на этом устройстве будет пересобрана по самым свежим данным с + сервера. Так решаются расхождения синхронизации и восстанавливается правильная + работа. This will recreate chunks for all files. If there were missing chunks, this may fix the errors.: Это пересоздаст чанки для всех файлов. Если какие-то чанки отсутствовали, это может исправить ошибки. +To minimise the creation of new conflicts: Чтобы создавалось меньше новых конфликтов Transfer Tweak: Настройки передачи +TURN Credential: Пароль TURN +TURN server settings are only necessary if you are behind a strict NAT or firewall that prevents direct P2P connections. In most cases, you can leave these fields blank.: + Настройки сервера TURN нужны только за строгим NAT или межсетевым экраном, + который не даёт устройствам соединяться напрямую. Обычно эти поля можно + оставить пустыми. +TURN Server URLs (comma-separated): Адреса серверов TURN (через запятую) +TURN Username: Имя пользователя TURN TweakMismatchResolve: Action: + DisableAutoAcceptCompatible: Отключить авто-приём Dismiss: Отмена + EnableAutoAcceptCompatible: Включить авто-приём UseConfigured: Использовать настроенные параметры UseMine: Обновить настройки удалённой базы данных UseMineAcceptIncompatible: Обновить настройки, но оставить как есть @@ -1005,9 +1801,49 @@ TweakMismatchResolve: UseRemoteAcceptIncompatible: Применить настройки, но игнорировать несовместимость UseRemoteWithRebuild: Применить настройки и загрузить снова Message: - Main: Настройки в удалённой базе данных следующие. Эти значения настроены - другими устройствами. - MainTweakResolving: Ваша конфигурация не совпадает с удалённым сервером. + AutoAcceptCompatibleUndefined: >- + + Похоже, настройки на устройствах различаются. Совместимые изменения можно + применять автоматически. + + Включить параметр `auto-accept`? + Main: >- + + Настройки в удалённой базе данных следующие. Эти значения настроены + другими устройствами, которые хотя бы раз синхронизировались с этим + устройством. + + + Если вы хотите использовать эти настройки, выберите + %{TweakMismatchResolve.Action.UseConfigured}. + + Если вы хотите сохранить настройки этого устройства, выберите + %{TweakMismatchResolve.Action.Dismiss}. + + + ${table} + + + >[!TIP] + + > Если вы хотите синхронизировать все настройки, используйте + «Синхронизация настроек через Markdown» после применения минимальной + конфигурации этой функцией. + + + ${additionalMessage} + MainTweakResolving: |- + Ваша конфигурация не совпадает с конфигурацией на удалённом сервере. + + Следующие параметры должны совпадать: + + ${table} + + Сообщите нам своё решение. + + ${additionalMessage} + mineUpdated: Настройки устройства обновлены. + remoteUpdated: Настройки на сервере обновлены. UseRemote: WarningRebuildRecommended: Некоторые изменения совместимы, но могут потребовать дополнительного хранилища. Рекомендуется перестроение. @@ -1018,22 +1854,438 @@ TweakMismatchResolve: WarningIncompatibleRebuildRequired: Обнаружены значения, несовместимые с удалённой базой данных. Требуется перестроение. Table: - _value: |- + _value: |+ | Имя значения | Это устройство | На удалённом | |: --- |: ---- :|: ---- :| - Row: "| name | self | remote |" + ${rows} + + Row: "| ${name} | ${self} | ${remote} |" Title: _value: Обнаружено несоответствие конфигурации + AutoAcceptCompatible: Доступен авто-приём TweakResolving: Обнаружено несоответствие конфигурации UseRemoteConfig: Использовать удалённую конфигурацию +Ui: + Common: + LocalDatabaseInitialisationFailed: Self-hosted LiveSync не может + синхронизировать. Соберите отчёт, чтобы посмотреть подробный журнал. + Signal: + Caution: ОСТОРОЖНО + Danger: ОПАСНО + Notice: К СВЕДЕНИЮ + Warning: ВНИМАНИЕ + SomeFilesCouldNotBeSynchronised: Часть файлов синхронизировать не удалось. + Проверьте эти файлы. Соберите отчёт, чтобы посмотреть подробный журнал. + Settings: + Advanced: + LocalDatabaseTweak: Тонкая настройка локальной базы + MemoryCache: Кеш в памяти + TransferTweak: Тонкая настройка передачи + Common: + Analyse: Анализировать + Back: Назад + Check: Проверить + Configure: Настроить + Continue: Продолжить + Delete: Удалить + Fetch: Забрать с сервера + Lock: Заблокировать + Merge: Объединить + Open: Открыть + Overwrite: Перезаписать + Perform: Выполнить + ResetAll: Сбросить всё + ResolveAll: Разрешить всё + Scan: Сканировать + Send: Отправить + Use: Использовать + VerifyAll: Проверить все + CustomizationSync: + OpenDesc: Открыть диалог + Panel: Синхронизация настроек + WarnChangeDeviceName: Пока эта функция включена, имя устройства сменить нельзя. + Отключите её, чтобы сменить имя. + WarnSetDeviceName: Задайте имя устройства, чтобы отличать его от других. Имя + должно быть уникальным среди ваших устройств. Без имени функцию включить + нельзя. + Hatch: + AnalyseDatabaseUsage: Проанализировать занятое место в базе + AnalyseDatabaseUsageDesc: Разбирает, чем занята база, и делает TSV-отчёт для + самостоятельной диагностики. Отчёт можно вставить в любую таблицу. + BackToNonConfigured: Вернуться к ненастроенному состоянию + ConvertNonObfuscated: Найти и преобразовать файлы без обфускации путей + ConvertNonObfuscatedDesc: Проверяет локальную базу на файлы, сохранённые без + обфускации путей, и при необходимости преобразует их. + CopyIssueReport: Скопировать отчёт в буфер обмена + DatabaseLabel: "База: ${details}" + DatabaseToStorage: База данных -> Файлы + DeleteCustomizationSyncData: Удалить все данные синхронизации настроек + GeneratedReport: Готовый отчёт + Missing: Отсутствует + ModifiedSize: "Изменён: ${modified}, размер: ${size}" + ModifiedSizeActual: "Изменён: ${modified}, размер: ${size} (фактический размер: + ${actualSize})" + PrepareIssueReport: Собрать «отчёт» для заведения issue + RecoveryAndRepair: Восстановление и исправление + RecreateAll: Пересоздать все + RecreateMissingChunks: Пересоздать недостающие чанки для всех файлов + RecreateMissingChunksDesc: Пересоздаёт чанки для всех файлов. Если каких-то + чанков не хватало, это может исправить ошибки. + ResetPanel: Сброс + ResetRemoteUsage: Сбросить порог уведомления и проверить занятое место в удалённой базе + ResetRemoteUsageDesc: Сбрасывает порог размера удалённого хранилища и снова + проверяет его размер. + ResolveAllConflictedFiles: Разрешить все конфликтующие файлы в пользу более новой версии + ResolveAllConflictedFilesDesc: "Разрешает все конфликтующие файлы в пользу более + новой версии. Внимание: старая версия будет перезаписана, вернуть её + нельзя." + RunDoctor: Запустить диагностику + ScanBrokenFiles: Найти повреждённые файлы + ScramSwitches: Аварийные выключатели + ShowHistory: Показать историю + StorageLabel: "Файлы: ${details}" + StorageToDatabase: Файлы -> База данных + VerifyAndRepairAllFiles: Проверить и восстановить все файлы + VerifyAndRepairAllFilesDesc: Сравнивает содержимое файлов в локальной базе и на + диске. Если они не совпадают, плагин спросит, какой вариант оставить. + Maintenance: + Cleanup: Выполнить очистку + CleanupDesc: "Освобождает место: удаляет все ревизии, кроме последней. Нужно + столько же свободного места на сервере и на устройстве." + DeleteLocalDatabase: Удалить локальную базу, чтобы сбросить или удалить Self-hosted LiveSync + EmergencyRestart: Аварийный перезапуск + EmergencyRestartDesc: Отключает всю синхронизацию и перезапускает плагин. + FreshStartWipe: Стереть всё и начать заново + FreshStartWipeDesc: Удаляет все данные на сервере. + GarbageCollection: Garbage Collection V3 (бета) + GarbageCollectionAction: Выполнить Garbage Collection + GarbageCollectionDesc: Garbage Collection убирает неиспользуемые чанки и уменьшает размер базы. + LockServer: Заблокировать сервер + LockServerDesc: Блокирует сервер, чтобы другие устройства не синхронизировались. + OverwriteRemote: Перезаписать удалённую базу + OverwriteRemoteDesc: Перезаписывает удалённую базу локальной базой и парольной фразой. + OverwriteServerData: Перезаписать данные на сервере файлами этого устройства + OverwriteServerDataDesc: Пересобирает локальную и удалённую базу из файлов этого устройства. + PurgeAllJournalCounter: Очистить все счётчики журнала + PurgeAllJournalCounterDesc: Очищает все кеши загрузки и отправки. + RebuildingOperations: Операции пересборки (только удалённая база) + Resend: Отправить заново + ResendDesc: Отправляет все чанки в удалённую базу заново. + Reset: Сброс + ResetAllJournalCounter: Сбросить все счётчики журнала + ResetAllJournalCounterDesc: Обнуляет всю историю журнала. При следующей + синхронизации каждый элемент будет получен и отправлен заново. + ResetJournalReceived: Сбросить историю полученных записей журнала + ResetJournalReceivedDesc: Обнуляет историю полученных записей журнала. При + следующей синхронизации всё, кроме отправленного этим устройством, будет + загружено заново. + ResetJournalSent: Сбросить историю отправленных записей журнала + ResetJournalSentDesc: Обнуляет историю отправленных записей журнала. При + следующей синхронизации всё, кроме полученного этим устройством, будет + отправлено заново. + ResetLocalSyncInfo: Сбросить данные синхронизации + ResetLocalSyncInfoDesc: Восстанавливает или пересобирает локальную базу из удалённой. + ResetReceived: Сбросить полученное + ResetSentHistory: Сбросить историю отправленного + ResetThisDevice: Сбросить синхронизацию на этом устройстве + ScheduleAndRestart: Запланировать и перезапустить + Scram: Аварийная остановка! + SendChunks: Отправить чанки + Syncing: Синхронизация + WarningLockedReadyAction: Я готов, разблокировать базу + WarningLockedReadyText: Чтобы хранилище не испортилось, удалённая база + заблокирована для синхронизации. (Это устройство помечено как + «разрешённое».) Когда все ваши устройства будут помечены как + «разрешённые», разблокируйте базу. Предупреждение будет появляться, пока + репликация не подтвердит, что устройство разрешено. + WarningLockedResolveAction: Я сделал резервную копию, отметить это устройство как разрешённое + WarningLockedResolveText: "Удалённая база заблокирована для синхронизации, чтобы + хранилище не испортилось: это устройство не помечено как «разрешённое». + Сделайте резервную копию хранилища, сбросьте локальную базу и выберите + «Отметить это устройство как разрешённое». Предупреждение будет + появляться, пока репликация не подтвердит, что устройство разрешено." + WriteRedFlagAndRestart: Поставить флаг и перезапустить + Patches: + CompatibilityConflict: Совместимость (поведение при конфликтах) + CompatibilityDatabase: Совместимость (структура базы) + CompatibilityInternalApi: Совместимость (использование внутреннего API) + CompatibilityMetadata: Совместимость (метаданные) + CompatibilityRemote: Совместимость (удалённая база) + CompatibilityTrouble: Совместимость (исправленные проблемы) + CurrentAdapter: "Текущий адаптер: ${adapter}" + DatabaseAdapter: Адаптер базы данных + DatabaseAdapterDesc: Выберите адаптер базы данных. + EdgeCaseBehaviour: Обработка крайних случаев (поведение) + EdgeCaseDatabase: Обработка крайних случаев (база данных) + EdgeCaseProcessing: Обработка крайних случаев (обработка) + IndexedDbWarning: Адаптер IndexedDB в некоторых случаях работает заметно + быстрее, но в режиме LiveSync он вызывает утечки памяти. В режиме + LiveSync используйте адаптер IDB. + MigratingToIdb: Переносим все данные в IDB... + MigratingToIndexedDb: Переносим все данные в IndexedDB... + MigrationIdbCompleted: Перенос в IDB завершён. Obsidian сразу перезапустится с новой настройкой. + MigrationIdbCompletedFollowUp: Перенос в IDB завершён. Переключите адаптер и перезапустите Obsidian. + MigrationIndexedDbCompleted: Перенос в IndexedDB завершён. Obsidian сразу + перезапустится с новой настройкой. + MigrationIndexedDbCompletedFollowUp: Перенос в IndexedDB завершён. Переключите + адаптер и перезапустите Obsidian. + MigrationWarning: Смена этого параметра требует переноса данных и перезапуска + Obsidian, это может занять время. Сделайте резервную копию данных перед + началом. + OperationToIdb: в IDB + OperationToIndexedDb: в IndexedDB + Remediation: Исправление + RemediationChanged: Настройка исправления изменена + RemediationNoLimit: Ограничение не задано + RemediationRestarting: Настройка исправления изменена. Перезапускаем Obsidian... + RemediationRestartLater: Позже + RemediationRestartMessage: Настоятельно советуем перезапустить Obsidian. До + перезапуска часть изменений не действует, а окно может показывать + неверные данные. Перезапустить сейчас? + RemediationRestartNow: Перезапустить сейчас + RemediationSuffixChanged: Суффикс изменён. Открываем базу заново... + RemediationWithValue: "Ограничение: ${date} (${timestamp})" + RemoteDatabaseSunset: Настройка удалённой базы (устаревает) + SwitchToIDB: Переключить на IDB + SwitchToIndexedDb: Переключить на IndexedDB + PowerUsers: + ConfigurationEncryption: Шифрование настроек + ConnectionTweak: Тонкая настройка соединения с CouchDB + ConnectionTweakDesc: Если при работе с IBM Cloudant вы упёрлись в предел размера + запроса, уменьшите размер пакета и лимит пакета. + Default: По умолчанию + Developer: Разработчик + EncryptSensitiveConfig: Шифровать важные параметры + PromptPassphraseEveryLaunch: Запрашивать парольную фразу при каждом запуске + UseCustomPassphrase: Использовать свою парольную фразу + Remote: + Activate: Включить + ActiveSuffix: " (активна)" + AddConnection: Добавить соединение + AddRemoteDefaultName: Новая удалённая база + ConfigureAndChangeRemote: Настроить и сменить удалённую базу + ConfigureE2EE: Настроить E2EE + ConfigureRemote: Настроить удалённую базу + DeleteRemoteConfirm: Удалить настройку удалённой базы «${name}»? + DeleteRemoteTitle: Удаление настройки удалённой базы + DisplayName: Отображаемое имя + DuplicateRemote: Дублировать удалённую базу + DuplicateRemoteSuffix: ${name} (копия) + E2EEConfiguration: Настройка E2EE + Export: Экспорт + FetchRemoteSettings: Забрать настройки с сервера + ImportConnection: Импорт соединения + ImportConnectionPrompt: Вставьте строку соединения + ImportedCouchDb: Импортированная CouchDB + ImportedRemote: Удалённая база + MoreActions: Ещё действия + PeerToPeerPanel: Синхронизация между устройствами (P2P) + RemoteConfigurationPrefix: Настройка удалённой базы + RemoteDatabases: Удалённые базы + RemoteName: Имя удалённой базы + RemoteNameCouchDb: CouchDB ${host} + RemoteNameP2P: P2P ${room} + RemoteNameS3: S3 ${bucket} + Rename: Переименовать + Selector: + AddDefaultPatterns: Добавить шаблоны по умолчанию + CrossPlatform: Кроссплатформенные + Default: По умолчанию + HiddenFiles: Скрытые файлы + IgnorePatterns: Шаблоны исключений + NonSynchronisingFiles: Несинхронизируемые файлы + NonSynchronisingFilesDesc: (RegExp) Если задано, изменения локальных и удалённых + файлов по этому шаблону пропускаются. + NormalFiles: Обычные файлы + OverwritePatterns: Шаблоны перезаписи + OverwritePatternsDesc: Шаблоны файлов, которые перезаписываются, а не сливаются + SynchronisingFiles: Синхронизируемые файлы + SynchronisingFilesDesc: (RegExp) Пусто — синхронизируются все файлы. Задайте + регулярное выражение, чтобы ограничить набор файлов. + TargetPatterns: Целевые шаблоны + TargetPatternsDesc: Шаблоны файлов для синхронизации + Setup: + RerunWizardButton: Запустить мастер заново + RerunWizardDesc: Запустить мастер первичной настройки, чтобы снова настроить + Self-hosted LiveSync. + RerunWizardName: Повторный запуск мастера настройки + SyncSettings: + Fetch: Забрать с сервера + Merge: Объединить + Overwrite: Перезаписать + SetupWizard: + ApplySettingsInitialisation: + ApplyWithoutInitialisation: Применить без инициализации + Back: Выбрать другой способ применить эти настройки + BypassGuidance: Если применить только настройки, устройство может стать + несовместимым с текущими данными синхронизации. Выбирайте это, только + если точно знаете, что пересборка не нужна. + BypassTitle: Применить настройки без инициализации? + ContinueFetch: Продолжить и забрать с сервера + FetchOption: Сбросить синхронизацию на этом устройстве + FetchOptionDesc: После перезапуска локальная база этого устройства + пересобирается из текущих данных удалённой базы. Затем файлы хранилища + сверяются с этими данными. + FetchOptionP2PDesc: После перезапуска выберите устройство-источник в сети. + Локальная база LiveSync этого устройства пересоберётся из него. + Guidance: Эти изменения меняют способ чтения данных синхронизации. Примените их + вместе с инициализацией после перезапуска. + KeepEditing: Продолжить правку + ProceedFetch: Перезапустить и забрать данные с сервера + ProceedFetchP2P: Перезапустить и выбрать устройство-источник + ProceedRebuild: Перезапустить и перезаписать данные на сервере + ProceedRebuildP2P: Перезапустить и подготовить это устройство + Question: Какие данные использовать после перезапуска? + RebuildOption: Перезаписать данные на сервере файлами этого устройства + RebuildOptionDesc: Пересобрать локальную и удалённую базы из текущих файлов + хранилища. Другим устройствам после этого нужно сбросить локальную + синхронизацию. + RebuildOptionP2P: Подготовить это устройство из этого хранилища + RebuildOptionP2PDesc: Пересобрать локальную базу LiveSync из текущих файлов + хранилища. Другое устройство при этом не перезаписывается. + RemoteVerificationGuidance: Не удалось проверить удалённую базу с текущими + учётными данными и настройками шифрования. Если продолжить, получение + данных после перезапуска может не сработать. + RemoteVerificationTitle: Не удалось проверить данные в удалённой базе + Title: Применить настройки и заново запустить синхронизацию + Common: + Back: Нет, вернуться назад + Cancel: Отмена + ProceedSelectOption: Выберите вариант, чтобы продолжить + Intro: + ExistingOption: Я добавляю устройство к готовой синхронизации + ExistingOptionDesc: Выберите это, если синхронизация уже работает на другом + компьютере или телефоне. Так вы подключите это устройство к готовой + связке. + Guidance: Сейчас несколько вопросов помогут упростить настройку синхронизации. + NewOption: Я настраиваю синхронизацию впервые + NewOptionDesc: Выберите это, если настраиваете это устройство как первое в + синхронизации. + ProceedExisting: Да, хочу добавить это устройство к готовой синхронизации + ProceedNew: Да, хочу настроить новую синхронизацию + Question: Сначала выберите вариант, который лучше описывает вашу ситуацию. + Title: Добро пожаловать в Self-hosted LiveSync + Invitation: + Start: Начать настройку + OutroAskUserMode: + CompatibleOption: Удалённая база уже настроена, и настройки совместимы (или + стали совместимыми после этой операции). + CompatibleOptionDesc: Если вы не уверены, этот вариант опасен. Он считает, что + настройки сервера совместимы с этим устройством. Если это не так, данные + могут пропасть. Убедитесь, что понимаете последствия. + ExistingOption: Мой сервер уже настроен. Хочу подключить к нему это устройство. + ExistingOptionDesc: С этим вариантом устройство подключается к готовому серверу. + Данные синхронизации нужно забрать с сервера на это устройство. + Guidance: Соединение с сервером настроено. Дальше нужно пересобрать локальную + базу, то есть данные синхронизации. + NewOption: Я настраиваю новый сервер впервые / хочу сбросить свой сервер. + NewOptionDesc: С этим вариантом сервер создаётся заново из текущих данных этого + устройства. Всё, что было на сервере, полностью перезаписывается. + ProceedApplySettings: Применить настройки + ProceedNext: Перейти к следующему шагу. + Question: Выберите свою ситуацию. + Title: "Почти готово: нужно ваше решение" + OutroNewP2PUser: + GuidanceNotice: В P2P нет центральной копии на сервере, которую можно + перезаписать. Этот шаг готовит только это устройство; держите его в + сети, пока другое устройство забирает первые данные. + GuidancePrimary: Соединение между устройствами настроено. Дальше локальная база + LiveSync соберётся из текущих файлов хранилища. + Important: ОБРАТИТЕ ВНИМАНИЕ + Proceed: Перезапустить и подготовить это устройство + Question: Нажмите кнопку ниже, чтобы перезапустить и перейти к подтверждению + локальной инициализации. + Title: "Настройка завершена: готовим это устройство к P2P" + OutroNewUser: + GuidancePrimary: Соединение с сервером настроено. Дальше данные синхронизации на + сервере соберутся из текущих данных этого устройства. + GuidanceWarning: После перезапуска данные этого устройства уйдут на сервер как + главная копия. Всё лишнее, что сейчас на сервере, будет полностью + перезаписано. + Important: ВАЖНО + Proceed: Перезапустить и инициализировать сервер + Question: Нажмите кнопку ниже, чтобы перезапустить и перейти к последнему + подтверждению. + Title: "Настройка завершена: готовимся инициализировать сервер" + RebuildEverythingP2P: + ConfirmLocalReset: Я понимаю, что это сбрасывает только локальную базу + синхронизации на этом устройстве. + ConfirmLocalResetNote: Она пересобирается из текущих файлов хранилища. + ConfirmTitle: ⚠️ Подтвердите следующее + Guidance: Эта операция удалит локальную базу LiveSync на этом устройстве и + соберёт её заново из текущих файлов хранилища. Данные на других + устройствах не удаляются и не перезаписываются. + Note: Держите это устройство в сети после инициализации, чтобы другое устройство + смогло забрать с него хранилище. + Proceed: Понимаю, подготовить это устройство + Title: "Последнее подтверждение: подготовка устройства к P2P" + SelectExisting: + Guidance: Вы добавляете это устройство к готовой синхронизации. + ManualOption: Настроить удалённую базу вручную + ManualOptionDesc: Настроить ту же удалённую базу вручную, как на других + устройствах. Только для опытных пользователей. + ProceedManual: Продолжить с ручной настройкой + ProceedQr: Отсканируйте камерой этого устройства QR-код с работающего устройства. + ProceedSetupUri: Продолжить с Setup URI + QrOption: Сканировать QR-код (советуем на телефоне) + QrOptionDesc: Отсканируйте камерой этого устройства QR-код с работающего устройства. + Question: Выберите способ, которым перенести настройки с другого устройства. + SetupUriOption: Использовать Setup URI (советуем) + SetupUriOptionDesc: Вставьте Setup URI, созданный на одном из работающих устройств. + Title: Способ настройки устройства + SelectNew: + Guidance: Сейчас настроим соединение для синхронизации. + ManualOption: Настроить удалённую базу вручную + ManualOptionDesc: "Вариант для опытных пользователей: если Setup URI нет или + нужны подробные настройки. Ещё так настраивают P2P вместо CouchDB или + объектного хранилища S3." + ProceedManual: Продолжить с ручной настройкой + ProceedSetupUri: Продолжить с Setup URI + Question: Как настроить это соединение для синхронизации? + SetupUriOption: Использовать Setup URI (советуем) + SetupUriOptionDesc: Setup URI — это одна строка с данными соединения и входа. + Если её выдал скрипт настройки, это простой и безопасный способ + настроить всё сразу. + Title: Способ соединения + SetupRemote: + BucketOption: Объектное хранилище, совместимое с S3 + BucketOptionDesc: "Синхронизация через файлы журнала. Нужно готовое объектное + хранилище с поддержкой S3: Amazon S3, MinIO или Cloudflare R2." + CouchDbOptionDesc: Самый подходящий способ синхронизации для нынешней + архитектуры плагина. Доступны все возможности. Нужен готовый сервер + CouchDB. + Guidance: Выберите тип удалённой базы для этой синхронизации. + P2POption: Между устройствами (P2P) + P2POptionDesc: Устройства синхронизируются напрямую. Сервер не нужен, но оба + устройства должны быть в сети одновременно, и часть возможностей может + не работать. Интернет нужен только для установки связи, данные через + него не идут. + ProceedBucket: Перейти к настройке объектного хранилища + ProceedCouchDb: Перейти к настройке CouchDB + ProceedP2P: Перейти к настройке P2P + Title: Выберите удалённую базу для синхронизации Unique name between all synchronized devices. To edit this setting, please disable customization sync once.: Уникальное имя между всеми синхронизируемыми устройствами. +Unless you are certain, selecting this options is bit dangerous. It assumes that the server configuration is compatible with this device. If this is not the case, data loss may occur. Please ensure you know what you are doing.: + Если вы не уверены, этот вариант опасен. Он считает, что настройки сервера + совместимы с этим устройством. Если это не так, данные могут пропасть. + Убедитесь, что понимаете, что делаете. +Updating list...: Обновляем список... +URL: URL Use a custom passphrase: Использовать пользовательскую парольную фразу +Use a Setup URI (Recommended): Использовать Setup URI (рекомендуется) Use Custom HTTP Handler: Использовать пользовательский HTTP обработчик +Use Diagnostic RTCPeerConnection for statistics: Использовать диагностический RTCPeerConnection для статистики Use dynamic iteration count: Использовать динамическое количество итераций +Use internal API: Использовать внутренний API +Use Internal API: Использовать внутренний API +Use JWT Authentication: Использовать аутентификацию JWT +Use Path-Style Access: Доступ в стиле пути (path-style) +Use Random Number: Использовать случайное число Use Segmented-splitter: Использовать сегментный разделитель Use splitting-limit-capped chunk splitter: Использовать разделитель чанков с ограничением Use the trash bin: Использовать корзину Use timeouts instead of heartbeats: Использовать таймауты вместо пульса +Use vrtmrz's relay: Использовать ретранслятор vrtmrz username: имя пользователя Username: Имя пользователя Verbose Log: Подробный лог @@ -1041,51 +2293,34 @@ Verify all: Проверить всё Verify and repair all files: Проверить и восстановить все файлы Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name.: Внимание! Это серьёзно повлияет на производительность. Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.: - Warning! This will have a serious impact on performance. And the logs will not - be synchronised under the default name. Please be careful with logs; they - often contain your confidential information. + "Внимание! Это серьёзно повлияет на производительность. Кроме того, логи не + будут синхронизироваться под именем по умолчанию. Пожалуйста, будьте осторожны + с логами: они часто содержат вашу конфиденциальную информацию." +WATCHING: СЛЕЖЕНИЕ +We can not use "/" to the device name: В имени устройства нельзя использовать «/» +We can use only Secure (HTTPS) connections on Obsidian Mobile.: На Obsidian Mobile работают только защищённые соединения (HTTPS). We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.: Невозможно изменить имя устройства, пока эта функция включена. Отключите её, чтобы изменить имя устройства. +We have to configure the device name: Нужно задать имя устройства +We recommend that you copy your Vault folder to a safe location. This will provide a safeguard in case a large number of conflicts arise, or if you accidentally synchronise with an incorrect destination.: + Советуем скопировать папку хранилища в надёжное место. Это защитит вас, если + появится много конфликтов или вы случайно синхронизируетесь не туда. +We will now guide you through a few questions to simplify the synchronisation setup.: Сейчас мы зададим несколько вопросов, чтобы упростить настройку синхронизации. +We will now proceed with the server configuration.: Теперь перейдём к настройке сервера. +Welcome to Self-hosted LiveSync: Добро пожаловать в Self-hosted LiveSync When you save a file in the editor, start a sync automatically: Когда вы сохраняете файл в редакторе, автоматически запускать синхронизацию Write credentials in the file: Записывать учётные данные в файл Write logs into the file: Записывать логи в файл xxhash32 (Fast but less collision resistance): xxhash32 (быстрый, но с меньшей устойчивостью к коллизиям) xxhash64 (Fastest): xxhash64 (самый быстрый) -"Welcome to Self-hosted LiveSync": "Добро пожаловать в Self-hosted LiveSync" -"We will now guide you through a few questions to simplify the synchronisation setup.": "Сейчас мы зададим несколько вопросов, чтобы упростить настройку синхронизации。" -"First, please select the option that best describes your current situation.": "Сначала выберите вариант, который лучше всего описывает вашу текущую ситуацию。" -"I am setting this up for the first time": "Я настраиваю это впервые" -"(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": "(Выберите этот вариант, если настраиваете это устройство как первое устройство синхронизации.) Он подходит, если вы впервые используете LiveSync и хотите настроить всё с нуля。" -"I am adding a device to an existing synchronisation setup": "Я добавляю устройство к существующей настройке синхронизации" -"(Select this if another device is already using LiveSync.) This option adds this device to that existing synchronisation setup.": "(Выберите этот вариант, если вы уже используете синхронизацию на другом компьютере или смартфоне.) Он подходит, если вы хотите добавить это устройство к уже существующей конфигурации LiveSync。" -"Yes, I want to set up a new synchronisation": "Да, я хочу настроить новую синхронизацию" -"Yes, I want to add this device to my existing synchronisation": "Да, я хочу добавить это устройство к существующей синхронизации" -"No, please take me back": "Нет, верните меня назад" -"Device Setup Method": "Способ настройки устройства" -"You are adding this device to an existing synchronisation setup.": "Вы добавляете это устройство к существующей настройке синхронизации。" -"Please select a method to import the settings from another device.": "Выберите способ импорта настроек с другого устройства。" -"Use a Setup URI (Recommended)": "Использовать Setup URI (рекомендуется)" -"Paste the Setup URI generated from one of your active devices.": "Вставьте Setup URI, созданный на одном из ваших активных устройств。" -"Scan a QR Code (Recommended for mobile)": "Сканировать QR-код (рекомендуется для мобильных устройств)" -"Scan the QR code displayed on an active device using this device's camera.": "Отсканируйте QR-код, показанный на активном устройстве, с помощью камеры этого устройства。" -"Enter the server information manually": "Ввести данные сервера вручную" -"Configure the same server information as your other devices again, manually, very advanced users only.": "Снова вручную укажите те же параметры сервера, что и на других устройствах. Только для очень опытных пользователей。" -"Proceed with Setup URI": "Продолжить с Setup URI" -"I know my server details, let me enter them": "Я знаю параметры сервера, позвольте ввести их вручную" -"Please select an option to proceed": "Чтобы продолжить, выберите вариант" -"Connection Method": "Способ подключения" -"We will now proceed with the server configuration.": "Теперь перейдём к настройке сервера。" -"How would you like to configure the connection to your server?": "Как вы хотите настроить подключение к серверу?" -"A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": "Setup URI — это одна строка текста, содержащая адрес сервера и данные аутентификации. Если URI был создан скриптом установки сервера, его использование обеспечивает простую и безопасную настройку。" -"This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": "Это расширенный вариант для пользователей, у которых нет URI или которые хотят вручную задать подробные параметры。" -"Enter Server Information": "Ввести данные сервера" -"Please select the type of server to which you are connecting.": "Выберите тип сервера, к которому вы подключаетесь。" -"Continue to CouchDB setup": "Перейти к настройке CouchDB" -"Continue to S3/MinIO/R2 setup": "Перейти к настройке S3/MinIO/R2" -"Continue to Peer-to-Peer only setup": "Перейти к настройке только Peer-to-Peer" -"This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": "Это наиболее подходящий для данной архитектуры способ синхронизации. Доступны все функции. Необходимо заранее развернуть экземпляр CouchDB。" -"S3/MinIO/R2 Object Storage": "Объектное хранилище S3/MinIO/R2" -"Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": "Синхронизация с использованием файлов журнала. Необходимо заранее настроить объектное хранилище, совместимое с S3/MinIO/R2。" -"Peer-to-Peer only": "Только Peer-to-Peer" -"This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": "Эта функция обеспечивает прямую синхронизацию между устройствами. Сервер не требуется, но для синхронизации оба устройства должны быть одновременно в сети, а некоторые функции могут быть ограничены. Подключение к Интернету нужно только для сигнализации (обнаружения пиров), а не для передачи данных。" +Yes, I want to add this device to my existing synchronisation: Да, я хочу добавить это устройство к существующей синхронизации +Yes, I want to set up a new synchronisation: Да, я хочу настроить новую синхронизацию +You are adding this device to an existing synchronisation setup.: Вы добавляете это устройство к существующей настройке синхронизации. +You can configure in the Obsidian Plugin Settings.: Настроить это можно в настройках плагина в Obsidian. +You should create a new synchronisation destination and rebuild your data there.: Создайте новое место для синхронизации и пересоберите данные там. +You should perform this operation only in exceptional circumstances, such as when the server data is completely corrupted, when changes on all other devices are no longer needed, or when the database size has become unusually large in comparison to the Vault size.: + "Делайте это только в исключительных случаях: данные на сервере полностью + испорчены, изменения на всех других устройствах больше не нужны или база стала + непомерно большой по сравнению с хранилищем." +you wanted(Thank you)!: которую вы хотели (спасибо!) From b7f38abae92ba5bc0315398d72da28c93bce097a Mon Sep 17 00:00:00 2001 From: Immick <12298761+Immick@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:03:51 +0300 Subject: [PATCH 02/58] fix(customisation-sync): stop truncating entry mtimes to 32 bits PluginDataExDisplayV2.mtime applied `~~` to the sum of millisecond timestamps before dividing, which keeps only the low 32 bits. The freshness chip and "Select All Shiny" then compared wrapped values and could offer an older copy as newer. Fixes #1194 --- .../CmdConfigSync.command.unit.spec.ts | 24 ++++++++++++++++++- src/features/ConfigSync/CmdConfigSync.ts | 3 ++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts b/src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts index 0237360c..5f524f8a 100644 --- a/src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts +++ b/src/features/ConfigSync/CmdConfigSync.command.unit.spec.ts @@ -52,7 +52,7 @@ vi.mock("@/common/obsidianCommunityPlugins.ts", () => ({ })); import { cancelTask } from "@/common/utils.ts"; -import { ConfigSync } from "./CmdConfigSync"; +import { ConfigSync, PluginDataExDisplayV2, type IPluginDataExDisplay } from "./CmdConfigSync"; describe("ConfigSync commands", () => { it("shows the Customisation Sync command only whilst the feature is enabled", () => { @@ -111,3 +111,25 @@ describe("ConfigSync commands", () => { expect(periodicPluginSweepProcessor.disable).toHaveBeenCalledOnce(); }); }); + +describe("PluginDataExDisplayV2", () => { + const entryWithModifiedTimes = (mtimes: number[]) => + new PluginDataExDisplayV2({ + documentPath: "ix:device/plugin_main/example", + category: "PLUGIN_MAIN", + name: "example", + term: "device", + files: mtimes.map((mtime, index) => ({ filename: `file-${index}`, mtime, data: [] })), + } as unknown as IPluginDataExDisplay); + + it("averages millisecond modification times without truncating them to 32 bits", () => { + const mtime = Date.UTC(2026, 8, 15); + expect(entryWithModifiedTimes([mtime, mtime, mtime]).mtime).toBe(mtime); + }); + + it("keeps a newer copy newer than an older one", () => { + const newer = entryWithModifiedTimes([Date.UTC(2026, 8, 15)]); + const older = entryWithModifiedTimes([Date.UTC(2026, 8, 2)]); + expect(newer.mtime).toBeGreaterThan(older.mtime); + }); +}); diff --git a/src/features/ConfigSync/CmdConfigSync.ts b/src/features/ConfigSync/CmdConfigSync.ts index 4201385f..b560dba6 100644 --- a/src/features/ConfigSync/CmdConfigSync.ts +++ b/src/features/ConfigSync/CmdConfigSync.ts @@ -368,7 +368,8 @@ export class PluginDataExDisplayV2 { return this._version; } get mtime(): number { - return ~~this.files.reduce((a, b) => a + b.mtime, 0) / this.files.length; + // Millisecond timestamps exceed 32 bits, so `~~` would wrap them. + return Math.floor(this.files.reduce((a, b) => a + b.mtime, 0) / this.files.length); } } export type PluginDataEx = { From f1746fa4b864197d297467e70511b6ae50940b78 Mon Sep 17 00:00:00 2001 From: Immick <12298761+Immick@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:03:51 +0300 Subject: [PATCH 03/58] fix(customisation-sync): hide identical items with "Hide not applicable items" The checkbox only toggled a CSS class for rows labelled "All the same or non-existent", while `hideNotApplicable`, which filters the offered devices by whether their copy can be applied, was hard-coded to false. Items present on another device therefore stayed visible even when every copy was the same. Drive `hideNotApplicable` from the checkbox, so the comparison only runs while it is on, and pass it to the reactive update so toggling it re-evaluates the rows. The term selection moves into PluginTerms.ts so it can be unit-tested. Fixes #1193 --- src/features/ConfigSync/PluginCombo.svelte | 31 +++++------ src/features/ConfigSync/PluginPane.svelte | 3 +- src/features/ConfigSync/PluginTerms.ts | 32 +++++++++++ .../ConfigSync/PluginTerms.unit.spec.ts | 54 +++++++++++++++++++ 4 files changed, 102 insertions(+), 18 deletions(-) create mode 100644 src/features/ConfigSync/PluginTerms.ts create mode 100644 src/features/ConfigSync/PluginTerms.unit.spec.ts diff --git a/src/features/ConfigSync/PluginCombo.svelte b/src/features/ConfigSync/PluginCombo.svelte index 7daba34a..91798795 100644 --- a/src/features/ConfigSync/PluginCombo.svelte +++ b/src/features/ConfigSync/PluginCombo.svelte @@ -12,6 +12,7 @@ // import { askString } from "../../common/utils"; import { Menu } from "@/deps.ts"; import { $msg as translateMessage } from "@/common/translation"; + import { selectSourceTerms } from "./PluginTerms.ts"; export let list: IPluginDataExDisplay[] = []; export let thisTerm = ""; @@ -182,24 +183,20 @@ } } - async function updateTerms(list: IPluginDataExDisplay[], selectNewest: boolean, isMaintenanceMode: boolean) { + async function updateTerms( + list: IPluginDataExDisplay[], + selectNewest: boolean, + isMaintenanceMode: boolean, + hideNotApplicable: boolean + ) { const local = list.find((e) => e.term == thisTerm); // selected = ""; - if (isMaintenanceMode) { - terms = [...new Set(list.map((e) => e.term))]; - } else if (hideNotApplicable) { - const termsTmp = []; - const wk = [...new Set(list.map((e) => e.term))]; - for (const termName of wk) { - const remote = list.find((e) => e.term == termName); - if ((await comparePlugin(local, remote)).canApply) { - termsTmp.push(termName); - } - } - terms = [...termsTmp]; - } else { - terms = [...new Set(list.map((e) => e.term))].filter((e) => e != thisTerm); - } + terms = await selectSourceTerms( + list, + thisTerm, + { isMaintenanceMode, hideNotApplicable }, + async (local, remote) => (await comparePlugin(local, remote)).canApply + ); let newest: IPluginDataExDisplay | undefined = local; if (selectNewest) { for (const term of terms) { @@ -230,7 +227,7 @@ } // currentSelectNewest = selectNewest; } - updateTerms(list, doSelectNewest, isMaintenanceMode); + updateTerms(list, doSelectNewest, isMaintenanceMode, hideNotApplicable); currentSelectNewest = selectNewest; } $: { diff --git a/src/features/ConfigSync/PluginPane.svelte b/src/features/ConfigSync/PluginPane.svelte index a6be2a6e..18a65109 100644 --- a/src/features/ConfigSync/PluginPane.svelte +++ b/src/features/ConfigSync/PluginPane.svelte @@ -32,7 +32,8 @@ export let core :LiveSyncBaseCore; // $: core = plugin.core; - $: hideNotApplicable = false; + // Comparing every copy is only worth it while the user asks to hide the unchanged ones. + $: hideNotApplicable = hideEven; $: thisTerm = core.services.setting.getDeviceAndVaultName(); const addOn = core.getAddOn(ConfigSync.name)!; diff --git a/src/features/ConfigSync/PluginTerms.ts b/src/features/ConfigSync/PluginTerms.ts new file mode 100644 index 00000000..71c1d503 --- /dev/null +++ b/src/features/ConfigSync/PluginTerms.ts @@ -0,0 +1,32 @@ +import type { IPluginDataExDisplay } from "./CmdConfigSync.ts"; + +export type CanApplyFrom = ( + local: IPluginDataExDisplay | undefined, + remote: IPluginDataExDisplay | undefined +) => Promise; + +/** + * Devices offered as sources for one item in the Customisation Sync dialogue. + * + * Maintenance mode lists every device, including this one. Otherwise every other + * device that has the item is listed; with `hideNotApplicable`, only devices whose + * copy can actually be applied remain, so an item that is the same everywhere ends + * up with no source and is shown as "All the same or non-existent". + */ +export async function selectSourceTerms( + list: IPluginDataExDisplay[], + thisTerm: string, + options: { isMaintenanceMode: boolean; hideNotApplicable: boolean }, + canApplyFrom: CanApplyFrom +): Promise { + const terms = [...new Set(list.map((e) => e.term))]; + if (options.isMaintenanceMode) return terms; + if (!options.hideNotApplicable) return terms.filter((term) => term != thisTerm); + const local = list.find((e) => e.term == thisTerm); + const applicable: string[] = []; + for (const term of terms) { + const remote = list.find((e) => e.term == term); + if (await canApplyFrom(local, remote)) applicable.push(term); + } + return applicable; +} diff --git a/src/features/ConfigSync/PluginTerms.unit.spec.ts b/src/features/ConfigSync/PluginTerms.unit.spec.ts new file mode 100644 index 00000000..f6fd714a --- /dev/null +++ b/src/features/ConfigSync/PluginTerms.unit.spec.ts @@ -0,0 +1,54 @@ +import { describe, expect, it, vi } from "vitest"; +import type { IPluginDataExDisplay } from "./CmdConfigSync.ts"; +import { selectSourceTerms } from "./PluginTerms.ts"; + +const copyOn = (term: string) => ({ term, files: [] }) as unknown as IPluginDataExDisplay; +const list = [copyOn("desktop"), copyOn("phone"), copyOn("tablet")]; +const differsOn = + (...terms: string[]) => + (_local: IPluginDataExDisplay | undefined, remote: IPluginDataExDisplay | undefined) => + Promise.resolve(terms.includes(remote?.term ?? "")); + +describe("selectSourceTerms", () => { + it("offers every other device without comparing copies by default", async () => { + const canApplyFrom = vi.fn(differsOn()); + const terms = await selectSourceTerms( + list, + "desktop", + { isMaintenanceMode: false, hideNotApplicable: false }, + canApplyFrom + ); + expect(terms).toEqual(["phone", "tablet"]); + expect(canApplyFrom).not.toHaveBeenCalled(); + }); + + it("offers every device, including this one, in maintenance mode", async () => { + const terms = await selectSourceTerms( + list, + "desktop", + { isMaintenanceMode: true, hideNotApplicable: true }, + differsOn() + ); + expect(terms).toEqual(["desktop", "phone", "tablet"]); + }); + + it("leaves out devices whose copy is the same when hiding items that are not applicable", async () => { + const terms = await selectSourceTerms( + list, + "desktop", + { isMaintenanceMode: false, hideNotApplicable: true }, + differsOn("tablet") + ); + expect(terms).toEqual(["tablet"]); + }); + + it("offers no source for an item that is the same on every device", async () => { + const terms = await selectSourceTerms( + list, + "desktop", + { isMaintenanceMode: false, hideNotApplicable: true }, + differsOn() + ); + expect(terms).toEqual([]); + }); +}); From 351da90f0c3ae73f09ed8dcf3662eb3fa8c68b91 Mon Sep 17 00:00:00 2001 From: speedy-axolotl <260110665+speedy-axolotl@users.noreply.github.com> Date: Sat, 26 Sep 2026 12:03:36 -0400 Subject: [PATCH 04/58] Improve Spanish translation --- src/common/messagesYAML/es.yaml | 248 ++++++++++++++++---------------- 1 file changed, 124 insertions(+), 124 deletions(-) diff --git a/src/common/messagesYAML/es.yaml b/src/common/messagesYAML/es.yaml index cdb2822a..8fa4b72a 100644 --- a/src/common/messagesYAML/es.yaml +++ b/src/common/messagesYAML/es.yaml @@ -1,32 +1,32 @@ ", please select the option that best describes the current state of your Vault. The application will then check your files in the most appropriate way based on your selection.": - ", selecciona la opción que mejor describa el estado actual de tu Vault. La + ", selecciona la opción que mejor describa el estado actual de tu bóveda. La aplicación comprobará entonces tus archivos de la forma más adecuada según tu elección." (Active): (Activo) (BETA) Always overwrite with a newer file: (BETA) Sobrescribir siempre con archivo más nuevo -(Beta) Use ignore files: (Beta) Usar archivos de ignorar +(Beta) Use ignore files: (Beta) Usar archivos ignore (Days passed, 0 to disable automatic-deletion): (Días transcurridos, 0 para desactivar) -(e.g., after editing many files whilst offline): (p. ej., tras editar muchos archivos sin conexión) +(e.g., after editing many files whilst offline): (p. ej., tras editar muchos archivos estando sin conexión) (e.g., immediately after restoring on another computer, or having recovered from a backup): (p. ej., justo después de restaurar en otro ordenador o de recuperar una copia de seguridad) (e.g., setting up for the first time on a new smartphone, starting from a clean slate): (p. ej., configurando por primera vez un móvil nuevo, empezando de cero) (ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.: - "(Ej: Leer chunks online) Lee chunks directamente en línea. Aumente tamaño de - chunks personalizados" + "(Ej: Leer chunks online) Lee chunks directamente en línea. Aumenta tamaño de + chunks personalizados." (MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.: - (MB) Saltar cambios en archivos locales/remotos mayores a este tamaño. Si se - reduce, se usará versión nueva + (MB) Si se activa, se saltarán cambios en archivos locales y remotos mayores a este tamaño. Si tamaño del archivo se reduce + se usará la versión nueva. (Mega chars): (Millones de caracteres) (Missing): (Falta) (Not recommended) If set, credentials will be stored in the file.: (No recomendado) Almacena credenciales en el archivo (Obsolete) Use an old adapter for compatibility: (Obsoleto) Usar adaptador antiguo (RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.: - (RegExp) Déjelo vacío para sincronizar todos los archivos. Defina un filtro - como expresión regular para limitar los archivos que se sincronizan. + (RegExp) Déjalo vacío para sincronizar todos los archivos. Definaeun filtro + como expresión regular para limitar los archivos sincronizados. (RegExp) If this is set, any changes to local and remote files that match this will be skipped.: - (RegExp) Si se establece, se omitirá cualquier cambio en archivos locales y - remotos que coincida con este patrón. + (RegExp) Si se establece, se omitirá cualquier cambio en los archivos locales y + remotos que coincidan con este patrón. "↑: Overwrite Remote": "↑: Sobrescribir remoto" "↓: Overwrite Local": "↓: Sobrescribir local" "⇅: Use newer": "⇅: Usar el más nuevo" @@ -59,22 +59,22 @@ Add default patterns: Añadir patrones predeterminados Add new connection: Añadir conexión AddOn Module (ConfigSync) has not been loaded. This is very unexpected situation. Please report this issue.: El módulo complementario (ConfigSync) no se ha cargado. Esta situación es muy - inesperada. Informa de este problema. + inesperada. Por favor informa de este problema. AddOn Module (HiddenFileSync) has not been loaded. This is very unexpected situation. Please report this issue.: El módulo complementario (HiddenFileSync) no se ha cargado. Esta situación es - muy inesperada. Informa de este problema. + muy inesperada. Por favor informa de este problema. Advanced: Avanzado Advanced Settings: Ajustes avanzados After restarting, the data on this device will be uploaded to the server as the 'master copy'. Please be aware that any unintended data currently on the server will be completely overwritten.: Tras reiniciar, los datos de este dispositivo se subirán al servidor como - «copia maestra». Ten en cuenta que cualquier dato no deseado que haya ahora en + «copia maestra». Ten en cuenta que cualquier dato en el servidor se sobrescribirá por completo. After restarting, the database on this device will be rebuilt using data from the server. If there are any unsynchronised files in this vault, conflicts may occur with the server data.: Tras reiniciar, la base de datos de este dispositivo se reconstruirá con los - datos del servidor. Si hay archivos sin sincronizar en este vault, pueden + datos del servidor. Si hay archivos sin sincronizar en esta bóveda pueden producirse conflictos con los datos del servidor. After that, synchronise to a brand new vault on each other device with the new remote one by one.: - Después, sincroniza con un vault totalmente nuevo en cada uno de los demás + Después, sincroniza con una bóveda totalmente nueva en cada uno de los demás dispositivos, uno por uno, usando el nuevo remoto. All checks passed successfully!: ¡Todas las comprobaciones se han superado correctamente! All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.: @@ -85,10 +85,10 @@ All the same or non-existent: Todo igual o inexistente Allow in session: Permitir en esta sesión Allow permanently: Permitir permanentemente Also, please note that if you are using Peer-to-Peer synchronization, this configuration will be used when you switch to other methods and connect to a remote server in the future.: - Además, si usas sincronización punto a punto, esta configuración se aplicará - cuando en el futuro cambies a otros métodos y te conectes a un servidor + Además, si usas sincronización punto a punto, esta configuración también se aplicará + en el futuro si cambias a otros métodos de sincronización y te conectas a un servidor remoto. -Always prompt merge conflicts: Siempre preguntar en conflictos +Always prompt merge conflicts: Siempre preguntar cuando haya conflictos Analyse: Analizar Analyse database usage: Analizar el uso de la base de datos Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.: @@ -96,7 +96,7 @@ Analyse database usage and generate a TSV report for diagnosis yourself. You can diagnosticarlo tú mismo. Puedes pegar el informe generado en la hoja de cálculo que prefieras. Apply All Selected: Aplicar todo lo seleccionado -Apply Latest Change if Conflicting: Aplicar último cambio en conflictos +Apply Latest Change if Conflicting: Aplicar último cambio si hay conflictos Apply preset configuration: Aplicar configuración predefinida Apply the settings: Aplicar los ajustes Ask a passphrase at every launch: Solicitar la frase de contraseña en cada inicio @@ -107,13 +107,13 @@ Automatically Sync all files when opening Obsidian.: Sincronizar automáticament Available Peers: Pares disponibles Back: Volver Back to non-configured: Volver a no configurado -Batch database update: Actualización por lotes de BD +Batch database update: Actualización de base de datos por lotes Batch limit: Límite de lotes Batch size: Tamaño de lote Batch size of on-demand fetching: Tamaño de lote para obtención bajo demanda Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.: - Antes de v0.17.16 usábamos adaptador antiguo. Nuevo adaptador requiere - reconstruir BD local. Desactive cuando pueda + Antes de v0.17.16 se usaba el adaptador antiguo. El nuevo adaptador requiere + reconstruir la base de datos local. Por favor desactiva esta opción cuando puedas. Si está activa cuando se sincroniza la base de datos se intentará desactivar de nuevo. Broadcasting?: ¿Difusión? Bucket Name: Nombre del bucket by resetting the remote, you will be informed on other devices.: al restablecer el remoto, se te avisará en los demás dispositivos. @@ -144,7 +144,7 @@ cmdConfigSync: Comma separated `.gitignore, .dockerignore`: "Separados por comas: `.gitignore, .dockerignore`" Command: Comando Communicating: Comunicando -Compaction in progress on remote database...: Compactación en curso en la base de datos remota... +Compaction in progress on remote database...: Compactación en curso de la base de datos remota... Compaction on remote database completed successfully.: La compactación de la base de datos remota se ha completado correctamente. Compaction on remote database failed.: La compactación de la base de datos remota ha fallado. Compaction on remote database timed out.: La compactación de la base de datos remota ha superado el tiempo de espera. @@ -229,7 +229,7 @@ Diagnostic RTCPeerConnection is enabled: El RTCPeerConnection de diagnóstico es dialog: yourLanguageAvailable: _value: >- - Self-hosted LiveSync tenía traducciones para tu idioma, así que se ha + Self-hosted LiveSync tiene traducciones para tu idioma, así que se ha activado el ajuste %{Display Language}. @@ -241,7 +241,7 @@ dialog: hacerlo desde el diálogo de ajustes. ¡Que lo disfrutes! - Title: " ¡Hay traducción disponible!" + Title: " ¡Hay una traducción disponible!" btnRevertToDefault: Mantener %{lang-def} Diff: Diferencias Different: Distinto @@ -271,13 +271,13 @@ Doctor: Por desgracia, se han detectado algunos ajustes como posibles problemas. Tranquilo: los resolveremos uno a uno. - Para que lo sepas de antemano, te preguntaremos por los siguientes puntos. + Para que lo sepas de antemano, te preguntaremos por lo siguiente. ${issues} ¿Empezamos? MainFix: |- - + ## ${name} | Actual | Ideal | @@ -303,8 +303,8 @@ Doctor: Recommended: Recomendado Message: NoIssues: ¡No se ha detectado ningún problema! - RebuildLocalRequired: ¡Atención! Hay que reconstruir la base de datos local para aplicar esto. - RebuildRequired: ¡Atención! Hay que reconstruir para aplicar esto. + RebuildLocalRequired: ¡Atención! Hay que reconstruir la base de datos local + RebuildRequired: ¡Atención! Hay que reconstruir la base de datos SomeSkipped: Hemos dejado algunos problemas sin resolver. ¿Quieres que te lo pregunte de nuevo en el próximo inicio? RULES: @@ -338,11 +338,11 @@ Enabling end-to-end encryption ensures that your data is encrypted on your devic Activar el cifrado de extremo a extremo garantiza que tus datos se cifren en tu dispositivo antes de enviarse al servidor remoto. Así, aunque alguien acceda al servidor, no podrá leer tus datos sin la frase de contraseña. - Recuérdala bien, porque será necesaria para descifrar tus datos en los demás + Recuérdala bien porque será necesaria para descifrar tus datos en los demás dispositivos. Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.: Cifrar contenido en la base de datos remota. Se recomienda habilitar si usa la - sincronización del plugin. + sincronización del complemento. Encrypting sensitive configuration items: Cifrando elementos sensibles Encryption Algorithm: Algoritmo de cifrado Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.: @@ -377,7 +377,7 @@ Failed to connect to remote for compaction. ${reason}: No se pudo conectar al re Failed to connect to the server. Please check your settings.: No se pudo conectar al servidor. Revisa tus ajustes. "Failed to connect to the signalling relay: ${reason}": "No se pudo conectar al relé de señalización: ${reason}" Failed to create replicator instance.: No se pudo crear la instancia del replicador. -Failed to parse Setup-URI.: No se pudo interpretar el Setup-URI. +Failed to parse Setup-URI.: No se pudo interpretar el Setup URI. Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.: No se pudo iniciar la replicación puntual previa a la recolección de basura. Recolección de basura cancelada. @@ -385,7 +385,7 @@ Failed to start replication after Garbage Collection.: No se pudo iniciar la rep "Failed:": "Fallidas:" Fetch: Obtener Fetch chunks on demand: Obtener chunks bajo demanda -Fetch database with previous behaviour: Obtener BD con comportamiento anterior +Fetch database with previous behaviour: Obtener base de datos con comportamiento anterior Fetch remote settings: Obtener ajustes remotos FETCHING: OBTENIENDO Fetching status...: Obteniendo el estado... @@ -401,7 +401,7 @@ Flag and restart: Marcar y reiniciar Flagged Selective: Selectivo marcado Folder Prefix: Prefijo de carpeta For HS256/HS512 algorithms, provide the shared secret key. For ES256/ES512 algorithms, provide the pkcs8 PEM-formatted private key.: - Para los algoritmos HS256/HS512, indica la clave secreta compartida. Para + Indica la clave secreta compartida para los algoritmos HS256/HS512. Para ES256/ES512, indica la clave privada en formato PEM pkcs8. Forces the file to be synced when opened.: Forzar sincronización al abrir archivo Fresh Start Wipe: Borrado para reinicio completo @@ -444,9 +444,9 @@ However, This should not be enabled if you want to increase your secrecy more.: I am setting up a new server for the first time / I want to reset my existing server.: Estoy configurando un servidor nuevo por primera vez / quiero restablecer mi servidor actual. -I am unable to create a backup of my Vault.: No puedo crear una copia de seguridad de mi Vault. -I am unable to create a backup of my Vaults.: No puedo crear una copia de seguridad de mis Vaults. -I have created a backup of my Vault.: He creado una copia de seguridad de mi Vault. +I am unable to create a backup of my Vault.: No puedo crear una copia de seguridad de mi bóveda. +I am unable to create a backup of my Vaults.: No puedo crear una copia de seguridad de mis bóvedas. +I have created a backup of my Vault.: He creado una copia de seguridad de mi bóveda. I understand that all changes made on other smartphones or computers possibly could be lost.: Entiendo que todos los cambios hechos en otros móviles u ordenadores podrían perderse. @@ -457,18 +457,18 @@ I understand that this action is irreversible once performed.: Entiendo que esta I understand the risks and will proceed without a backup.: Entiendo los riesgos y continuaré sin copia de seguridad. I Understand, Overwrite Server: Lo entiendo, sobrescribir el servidor If "Auto Start P2P Connection" is enabled, the P2P connection will be started automatically when the plug-in launches.: - Si «Iniciar la conexión P2P automáticamente» está activado, la conexión P2P se + Si «Iniciar la conexión P2P automáticamente» está activada, la conexión P2P se iniciará al arrancar el complemento. -If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).: Si se desactiva, chunks se dividen en hilo UI (comportamiento anterior) +If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).: Si se desactiva, los chunks se dividen en el hilo de UI (comportamiento anterior) If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.: Habilita sincronización eficiente por archivo. Requiere migración y actualizar - todos dispositivos a v0.23.18. Pierde compatibilidad con versiones antiguas + todos los dispositivos a v0.23.18. Pierde compatibilidad con versiones antiguas. If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.: Divide chunks en máximo 100 ítems. Menos eficiente en deduplicación -If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.: Chunks nuevos se mantienen temporalmente en el documento hasta estabilizarse +If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.: Chunks nuevos se mantienen temporalmente en el documento hasta estabilizarse. If enabled, the ⛔ icon will be shown inside the status instead of the file warnings banner. No details will be shown.: Si se activa, se mostrará el icono ⛔ en el estado en lugar del banner de advertencia de archivos. No se mostrarán detalles. -If enabled, the file under 1kb will be processed in the UI thread.: Archivos <1kb se procesan en hilo UI +If enabled, the file under 1kb will be processed in the UI thread.: Archivos <1kb se procesan en el hilo de UI If enabled, the notification of hidden files change will be suppressed.: Si se habilita, se suprimirá la notificación de cambios en archivos ocultos. If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour): Si se habilita, todos los chunks se almacenan con la revisión hecha desde su @@ -476,13 +476,13 @@ If this enabled, all chunks will be stored with the revision made from its conte If this enabled, All files are handled as case-Sensitive (Previous behaviour).: Si se habilita, todos los archivos se manejan como sensibles a mayúsculas (comportamiento anterior) -If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.: Divide chunks en segmentos semánticos. No todos los sistemas lo soportan +If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.: Divide chunks en segmentos semánticos. No todos los sistemas lo soportan. If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.: Saltar cambios en archivos locales que coincidan con ignore files. Cambios - remotos usan ignore files locales + remotos usan ignore files locales. If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.: - Mantiene conexión 60s. Si no hay cambios, reinicia socket. Útil con proxies - limitantes + Mantiene una conexión abierta por 60s en CouchDB. Si no hay cambios, se reinicia socket en vez de dejarlo abierto de forma indefinida. Útil con proxies + limitantes pero puede aumentar el uso de recursos. If you cannot avoid CORS issues, you might want to try this option. It uses Obsidian's internal API to communicate with the CouchDB server. Not compliant with web standards, but works. Note that this might break in future Obsidian versions.: Si no puedes evitar problemas de CORS, prueba esta opción. Usa la API interna de Obsidian para comunicarse con el servidor CouchDB. No cumple los estándares @@ -494,11 +494,11 @@ If you cannot avoid CORS issues, you might want to try this option. It uses Obsi pero funciona. Ten en cuenta que podría dejar de funcionar en versiones futuras de Obsidian. If you have unsynchronised changes in your Vault on this device, they will likely diverge from the server's versions after the reset. This may result in a large number of file conflicts.: - Si tienes cambios sin sincronizar en el Vault de este dispositivo, es probable - que divergan de las versiones del servidor tras el restablecimiento. Esto + Si tienes cambios sin sincronizar en la bóveda de este dispositivo, es probable + que diverjan de las versiones del servidor tras el restablecimiento. Esto puede provocar un gran número de conflictos de archivos. If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value.: - Si alcanzas el límite de tamaño de carga al usar IBM Cloudant, reduce el + Si alcanzas el límite de tamaño de carga al usar IBM Cloudant, por favor reduce el tamaño de lote y el límite de lote. If you understand the risks and still wish to proceed, select so.: Si entiendes los riesgos y aun así quieres continuar, indícalo. If you want to store the data in a specific folder within the bucket, you can specify a folder prefix here. Otherwise, leave it blank to store data at the root of the bucket.: @@ -516,7 +516,7 @@ Import connection: Importar conexión IMPORTANT: IMPORTANTE In most cases, you should stick with the default algorithm (${algorithm}), This setting is only required if you have an existing Vault encrypted in a different format.: En la mayoría de los casos deberías mantener el algoritmo predeterminado - (${algorithm}). Este ajuste solo es necesario si ya tienes un Vault cifrado en + (${algorithm}). Este ajuste solo es necesario si ya tienes una bóveda cifrada en otro formato. In this scenario, Self-hosted LiveSync will recreate metadata for every file and deliberately generate conflicts. Where the file content is identical, these conflicts will be resolved automatically.: En este caso, Self-hosted LiveSync recreará los metadatos de todos los @@ -712,12 +712,12 @@ moduleInputUIObsidian: optionNo: No optionYes: Sí moduleLiveSyncMain: - logAdditionalSafetyScan: Escanéo de seguridad adicional... + logAdditionalSafetyScan: Escaneo de seguridad adicional... logLoadingPlugin: Cargando complemento... logPluginInitCancelled: La inicialización del complemento fue cancelada por un módulo logPluginVersion: Self-hosted LiveSync v${manifestVersion} ${packageVersion} logReadChangelog: LiveSync se ha actualizado, ¡por favor lee el registro de cambios! - logSafetyScanCompleted: Escanéo de seguridad adicional completado + logSafetyScanCompleted: Escaneo de seguridad adicional completado logSafetyScanFailed: El escaneo de seguridad adicional ha fallado en un módulo logUnloadingPlugin: Descargando complemento... logVersionUpdate: LiveSync se ha actualizado, en caso de actualizaciones que @@ -749,7 +749,7 @@ moduleLiveSyncMain: o se obtiene. Si el proceso termina de forma anormal, puede mantenerse sin querer. - > Si no estás seguro, puedes intentar volver a ejecutar estos procesos. + > Si no estás seguro puedes intentar volver a ejecutar estos procesos. Asegúrate de hacer una copia de seguridad de tu bóveda. optionKeepLiveSyncDisabled: Mantener LiveSync desactivado optionResumeAndRestart: Reanudar y reiniciar Obsidian @@ -885,7 +885,7 @@ moduleMigration: Nota: actualiza Self-hosted LiveSync a la v0.25.6 o superior en todos tus - dispositivos y haz una copia de seguridad fiable de tu vault. + dispositivos y haz una copia de seguridad fiable de tu bóveda. Nota 2: reconstruir todo y obtener los datos consume algo de tiempo y de tráfico; hazlo en horas de poco uso y con una conexión de red estable. @@ -934,7 +934,7 @@ On the source device, from the command palette, run the 'Show settings as a QR c En el dispositivo de origen, ejecuta desde la paleta de comandos «Show settings as a QR code». On the source device, open Obsidian.: En el dispositivo de origen, abre Obsidian. -On this device, please keep this Vault open.: En este dispositivo, mantén este Vault abierto. +On this device, please keep this Vault open.: En este dispositivo, mantén esta bóveda abierta. On this device, switch to the camera app or use a QR code scanner to scan the displayed QR code.: En este dispositivo, cambia a la aplicación de cámara o usa un lector de QR para escanear el código mostrado. @@ -956,7 +956,7 @@ P2P: Note: Summary: ¿Qué es esta función? (incluye notas importantes; léelas al menos una vez) description: >2- - Este replicador permite sincronizar el vault con otros dispositivos + Este replicador permite sincronizar la bóveda con otros dispositivos mediante una conexión punto a punto. Así se puede sincronizar con nuestros otros dispositivos sin usar un servicio en la nube. @@ -1015,7 +1015,7 @@ Please enter the details required to connect to your S3/MinIO/R2 compatible obje Please enter the Peer-to-Peer Synchronisation information below.: Introduce a continuación los datos de la sincronización punto a punto. Please enter the Setup URI that was generated during server installation or on another device, along with the vault passphrase.: Introduce el Setup URI generado durante la instalación del servidor o en otro - dispositivo, junto con la frase de contraseña del vault. + dispositivo, junto con la frase de contraseña de la bóveda. Please follow the steps below to import settings from your existing device.: Sigue los pasos siguientes para importar los ajustes desde tu dispositivo actual. @@ -1259,7 +1259,7 @@ Setup: LabelEncryptionAlgorithm: Algoritmo de cifrado DefaultAlgorithmDesc: En la mayoría de los casos, debes mantener el algoritmo predeterminado (${algorithm}). Este ajuste solo es necesario si ya tienes - un Vault cifrado con un formato diferente. + una bóveda cifrada con un formato diferente. AlgorithmWarning: Cambiar el algoritmo de cifrado impedirá el acceso a cualquier dato cifrado anteriormente con otro algoritmo. Asegúrate de que todos tus dispositivos estén configurados para usar el mismo algoritmo y así @@ -1278,16 +1278,16 @@ Setup: Title: Introducir URI de configuración GuidanceLine1: Introduce la URI de configuración que se generó durante la instalación del servidor o en otro dispositivo, junto con la frase de - contraseña del Vault. + contraseña de la bóveda. GuidanceLine2: Ten en cuenta que puedes generar una nueva URI de configuración ejecutando el comando "Copiar ajustes como nueva URI de configuración" desde la paleta de comandos. LabelSetupURI: URI de configuración ValidInfo: La URI de configuración es válida y está lista para usarse. InvalidInfo: La URI de configuración no es válida. Revísala e inténtalo de nuevo. - LabelPassphrase: Frase de contraseña del Vault - PlaceholderPassphrase: Introduce la frase de contraseña del Vault - ErrorPassphraseRequired: Introduce la frase de contraseña del Vault. + LabelPassphrase: Frase de contraseña de la bóveda + PlaceholderPassphrase: Introduce la frase de contraseña de la bóveda + ErrorPassphraseRequired: Introduce la frase de contraseña de la bóveda. ErrorFailedToParse: No se pudo procesar la URI de configuración. Revisa la URI y la frase de contraseña. ButtonProceed: Probar ajustes y continuar @@ -1296,7 +1296,7 @@ Setup: Title: Escanear código QR Guidance: Sigue los pasos de abajo para importar los ajustes desde tu dispositivo actual. - Step1: En este dispositivo, mantén este Vault abierto. + Step1: En este dispositivo, mantén esta bóveda abierta. Step2: En el dispositivo de origen, abre Obsidian. Step3: En el dispositivo de origen, ejecuta desde la paleta de comandos la orden "Mostrar ajustes como código QR". @@ -1559,7 +1559,7 @@ obsidianLiveSyncSettingTab: cifrado? msgFetchConfigFromRemote: ¿Quieres obtener la configuración del servidor remoto? msgIfConfigNotPersistent: Si la configuración del servidor no es persistente - (por ejemplo, ejecutándose en docker), los valores aquí pueden cambiar. Una + (por ejemplo, ejecutándose en Docker), los valores aquí pueden cambiar. Una vez que puedas conectarte, por favor actualiza las configuraciones en el local.ini del servidor. msgInvalidPassphrase: Tu frase de contraseña de cifrado podría ser inválida. @@ -1669,14 +1669,14 @@ obsidianLiveSyncSettingTab: okAdminPrivileges: ✔ Tienes privilegios de administrador. okCorsCredentials: ✔ cors.credentials está correcto. okCorsCredentialsForOrigin: CORS credenciales OK - okCorsOriginMatched: ✔ Origen de CORS correcto - okCorsOrigins: ✔ cors.origins está correcto. - okEnableCors: ✔ httpd.enable_cors está correcto. - okMaxDocumentSize: ✔ couchdb.max_document_size está correcto. - okMaxRequestSize: ✔ chttpd.max_http_request_size está correcto. - okRequireValidUser: ✔ chttpd.require_valid_user está correcto. - okRequireValidUserAuth: ✔ chttpd_auth.require_valid_user está correcto. - okWwwAuth: ✔ httpd.WWW-Authenticate está correcto. + okCorsOriginMatched: ✔ Origen de CORS es correcto + okCorsOrigins: ✔ cors.origins es correcto. + okEnableCors: ✔ httpd.enable_cors es correcto. + okMaxDocumentSize: ✔ couchdb.max_document_size es correcto. + okMaxRequestSize: ✔ chttpd.max_http_request_size es correcto. + okRequireValidUser: ✔ chttpd.require_valid_user es correcto. + okRequireValidUserAuth: ✔ chttpd_auth.require_valid_user es correcto. + okWwwAuth: ✔ httpd.WWW-Authenticate es correcto. optionApply: Aplicar optionCancel: Cancelar optionCouchDB: CouchDB @@ -1689,7 +1689,7 @@ obsidianLiveSyncSettingTab: optionOnEvents: En eventos optionPeriodicAndEvents: Periódico y en eventos optionPeriodicWithBatch: Periódico con lote - optionRebuildBoth: Reconstructuir ambos desde este dispositivo + optionRebuildBoth: Reconstruir ambos desde este dispositivo optionSaveOnlySettings: (Peligro) Guardar solo configuración panelChangeLog: Registro de cambios panelGeneralSettings: Configuraciones generales @@ -1778,7 +1778,7 @@ Rebuild local and remote database with local files.: Reconstruye la base de dato Rebuilding Operations (Remote Only): Operaciones de reconstrucción (solo remoto) Recreate all: Recrear todo Recreate missing chunks for all files: Recrear fragmentos faltantes para todos los archivos -Reducing the frequency with which on-disk changes are reflected into the DB: Reducir frecuencia de actualizaciones de disco a BD +Reducing the frequency with which on-disk changes are reflected into the DB: Reducir frecuencia de actualizaciones de disco a base de datos Region: Región Remediation: Remediación Remediation Setting Changed: La configuración de remediación cambió @@ -1789,7 +1789,7 @@ Remote server type: Tipo de servidor remoto Remote Type: Tipo de remoto Rename: Renombrar Requires restart of Obsidian: Requiere reiniciar Obsidian -Requires restart of Obsidian.: Requiere reiniciar Obsidian +Requires restart of Obsidian.: Requiere reiniciar Obsidian. Resend: Reenviar Resend all chunks to the remote.: Reenvía todos los chunks al remoto. Reset: Restablecer @@ -1828,10 +1828,10 @@ Schedule and Restart: Programar y reiniciar Scram!: Medidas de emergencia Seconds, 0 to disable: Segundos, 0 para desactivar Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.: - Segundos. Guardado en BD local se retrasará hasta este valor tras dejar de - escribir/guardar + Segundos. Guardado en base de datos local se retrasará hasta este valor tras dejar de + escribir o guardar. Secret Key: Clave secreta -Select the database adapter to use.: Selecciona el adaptador de base de datos que se usará. +Select the database adapter to use.: Selecciona el adaptador de base de datos. Send: Enviar Send chunks: Enviar chunks Server URI: URI del servidor @@ -1947,7 +1947,7 @@ Stat: Estado Stats: Estadísticas Stop ⚡: Detener ⚡ Stop Broadcasting: Detener difusión -Stop reflecting database changes to storage files.: Dejar de reflejar cambios de BD en archivos +Stop reflecting database changes to storage files.: Dejar de reflejar cambios de base de datos en archivos Stop watching for file changes.: Dejar de monitorear cambios en archivos Storage -> Database: Almacenamiento -> Base de datos Strongly Recommended: Muy recomendado @@ -1981,7 +1981,7 @@ The connection to the server has been configured successfully. As the next step, La conexión con el servidor se ha configurado correctamente. Como paso siguiente, The delay for consecutive on-demand fetches: Retraso entre obtenciones consecutivas -The files in this Vault are almost identical to the server's.: Los archivos de este Vault son casi idénticos a los del servidor. +The files in this Vault are almost identical to the server's.: Los archivos de esta bóveda son casi idénticos a los del servidor. "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": >- A los siguientes nodos aceptados les falta su información de nodo: @@ -2026,7 +2026,7 @@ the synchronisation data on the server will be built based on the current data o actuales de este dispositivo. Themes: Temas There is a way to resolve this on other devices.: Hay una forma de resolver esto en los demás dispositivos. -There may be differences between the files in this Vault and the server.: Puede haber diferencias entre los archivos de este Vault y los del servidor. +There may be differences between the files in this Vault and the server.: Puede haber diferencias entre los archivos de esta bóveda y los del servidor. Therefore, we ask that you exercise extreme caution when configuring server information manually. If an incorrect passphrase is entered, the data on the server will become corrupted.: Por tanto, extrema la precaución al configurar manualmente los datos del servidor. Si introduces una frase de contraseña incorrecta, los datos del @@ -2042,23 +2042,23 @@ This first setup has several short steps because it confirms encryption, the con Una vez completada, los demás dispositivos podrán reutilizar un Setup URI. This is an extremely powerful operation. We strongly recommend that you copy your Vault folder to a safe location.: Esta es una operación extremadamente potente. Te recomendamos encarecidamente - copiar la carpeta de tu Vault a un lugar seguro. + copiar la carpeta de tu bóveda a un lugar seguro. This optional check uses Obsidian's internal request API and sends the credentials above to the CouchDB server. Use it only with a server you trust; administrator access may be required.: Esta comprobación opcional usa la API interna de solicitudes de Obsidian y envía las credenciales anteriores al servidor CouchDB. Utilízala solo con un servidor de confianza; puede requerir acceso de administrador. -This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.: Esta frase no se copia a otros dispositivos. Usará `Default` hasta reconfigurar +This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.: Esta frase no se copia a otros dispositivos. Usará `Default` hasta que lo configures de nuevo. This password is used to encrypt the connection. Use something long enough.: Esta contraseña se usa para cifrar la conexión. Usa algo suficientemente largo. This procedure will first delete all existing synchronisation data from the server. Following this, the server data will be completely rebuilt, using the current state of your Vault on this device (including its local database) as: Este procedimiento eliminará primero todos los datos de sincronización existentes en el servidor. A continuación, los datos del servidor se - reconstruirán por completo usando el estado actual del Vault de este + reconstruirán por completo usando el estado actual de la bóveda de este dispositivo (incluida su base de datos local) como This setting must be the same even when connecting to multiple synchronisation destinations.: Este ajuste debe ser el mismo incluso si te conectas a varios destinos de sincronización. This Vault is empty, or contains only new files that are not on the server.: - Este Vault está vacío o solo contiene archivos nuevos que no están en el + Esta bóveda está vacía o solo contiene archivos nuevos que no están en el servidor. This will rebuild the local database on this device using the most recent data from the server. This action is designed to resolve synchronisation inconsistencies and restore correct functionality.: Esto reconstruirá la base de datos local de este dispositivo con los datos más @@ -2090,13 +2090,13 @@ TweakMismatchResolve: UseRemoteWithRebuild: Aplicar los ajustes a este dispositivo y volver a obtener los datos Message: AutoAcceptCompatibleUndefined: >- - + Parece que los ajustes son distintos en cada dispositivo. Ahora se pueden aplicar automáticamente los cambios compatibles a estas configuraciones. ¿Quieres activar la aceptación automática (`auto-accept`)? Main: >- - + Los ajustes de la base de datos remota son los siguientes. Los han configurado otros dispositivos que se han sincronizado con este al menos una vez. @@ -2131,7 +2131,7 @@ TweakMismatchResolve: ${additionalMessage} UseRemote: WarningRebuildRecommended: >- - + >[!NOTICE] > Algunos cambios son compatibles, pero pueden consumir almacenamiento y @@ -2140,7 +2140,7 @@ TweakMismatchResolve: > ***Asegúrate de tener tiempo y una red estable antes de aplicarlo.*** WarningRebuildRequired: >- - + >[!WARNING] > Algunas configuraciones remotas no son compatibles con la base de @@ -2148,7 +2148,7 @@ TweakMismatchResolve: > ***Asegúrate de tener tiempo y una red estable antes de aplicarlo.*** WarningIncompatibleRebuildRecommended: >- - + >[!NOTICE] > Hemos detectado que algunos valores difieren de forma que hacen @@ -2161,7 +2161,7 @@ TweakMismatchResolve: > Si decides reconstruir, tardará unos minutos o más. **Asegúrate de que es seguro hacerlo ahora.** WarningIncompatibleRebuildRequired: >- - + >[!WARNING] > Hemos detectado que algunos valores difieren de forma que hacen @@ -2316,7 +2316,7 @@ Ui: SendChunks: Enviar los chunks Syncing: Sincronización WarningLockedReadyAction: Estoy listo, desbloquear la base de datos - WarningLockedReadyText: Para evitar que el vault se corrompa, la base de datos + WarningLockedReadyText: Para evitar que la bóveda se corrompa, la base de datos remota se ha bloqueado para la sincronización. (Este dispositivo está marcado como «resuelto».) Cuando todos tus dispositivos estén marcados como «resueltos», desbloquea la base de datos. Este aviso seguirá @@ -2324,9 +2324,9 @@ Ui: resuelto. WarningLockedResolveAction: He hecho una copia de seguridad, marcar este dispositivo como resuelto WarningLockedResolveText: La base de datos remota está bloqueada para la - sincronización a fin de evitar que el vault se corrompa, porque este + sincronización a fin de evitar que la bóveda se corrompa, porque este dispositivo no está marcado como «resuelto». Haz una copia de seguridad - de tu vault, restablece la base de datos local y selecciona «Marcar este + de tu bóveda, restablece la base de datos local y selecciona «Marcar este dispositivo como resuelto». Este aviso seguirá apareciendo hasta que la replicación confirme que el dispositivo está resuelto. WriteRedFlagAndRestart: Marcar y reiniciar @@ -2366,7 +2366,7 @@ Ui: RemediationNoLimit: Sin límite configurado RemediationRestartLater: Más tarde RemediationRestartMessage: Se recomienda encarecidamente reiniciar Obsidian. - Hasta que lo hagas, puede que algunos cambios no surtan efecto y que la + Hasta que lo hagas puede que algunos cambios no surtan efecto y que la interfaz se muestre de forma inconsistente. ¿Seguro que quieres reiniciar ahora? RemediationRestartNow: Reiniciar ahora @@ -2399,7 +2399,7 @@ Ui: DisplayName: Nombre visible DuplicateRemote: Duplicar el remoto DuplicateRemoteSuffix: ${name} (copia) - E2EEConfiguration: Configuración del E2EE + E2EEConfiguration: Configuración de E2EE Export: Exportar FetchRemoteSettings: Obtener los ajustes remotos ImportConnection: Importar conexión @@ -2493,7 +2493,7 @@ Ui: dispositivo obtenga sus datos iniciales. GuidancePrimary: La conexión punto a punto se ha configurado correctamente. A continuación, la base de datos local de LiveSync se construirá a partir - de los archivos actuales de este Vault. + de los archivos actuales de esta bóveda. Important: TEN EN CUENTA Proceed: Reiniciar y preparar este dispositivo Question: Pulsa el botón de abajo para reiniciar y pasar a la confirmación de la @@ -2513,13 +2513,13 @@ Ui: RebuildEverythingP2P: ConfirmLocalReset: Entiendo que esto restablece únicamente la base de datos de sincronización local de este dispositivo. - ConfirmLocalResetNote: Se usarán los archivos que hay ahora en este Vault para reconstruirla. + ConfirmLocalResetNote: Se usarán los archivos que hay ahora en esta bóveda para reconstruirla. ConfirmTitle: ⚠️ Confirma lo siguiente Guidance: Este procedimiento descartará la base de datos local de LiveSync de este dispositivo y la reconstruirá a partir de los archivos actuales de - este Vault. No elimina ni sobrescribe datos de otro dispositivo. + esta bóveda. No elimina ni sobrescribe datos de otro dispositivo. Note: Mantén este dispositivo conectado después de la inicialización para que - otro dispositivo pueda obtener el Vault desde él. + otro dispositivo pueda obtener la bóveda desde él. Proceed: Lo entiendo, preparar este dispositivo Title: "Confirmación final: preparar este dispositivo para P2P" SelectExisting: @@ -2560,7 +2560,7 @@ Ui: un servicio de almacenamiento de objetos compatible con S3, como Amazon S3, MinIO o Cloudflare R2. CouchDbOptionDesc: Es el método de sincronización más adecuado para el diseño - actual y ofrece todas las funciones. Necesitas tener ya una instancia de + actual y ofrece todas las funciones. Necesitas tener una instancia de CouchDB en marcha. Guidance: Selecciona el tipo de remoto para esta configuración de sincronización. P2POption: Punto a punto (P2P) @@ -2611,8 +2611,8 @@ We cannot change the device name while this feature is enabled. Please disable t habilitada. Deshabilita la función para cambiarlo. We have to configure the device name: Hay que configurar el nombre del dispositivo We recommend that you copy your Vault folder to a safe location. This will provide a safeguard in case a large number of conflicts arise, or if you accidentally synchronise with an incorrect destination.: - Te recomendamos copiar la carpeta de tu Vault a un lugar seguro. Así tendrás - una salvaguarda en caso de que surjan muchos conflictos o de que sincronices + Te recomendamos copiar la carpeta de tu bóveda a un lugar seguro. Así tendrás + una copia de seguridad en caso de que surjan muchos conflictos o de que sincronices por error con un destino incorrecto. When you save a file in the editor, start a sync automatically: Iniciar sincronización automática al guardar en editor Write credentials in the file: Escribir credenciales en archivo @@ -2621,60 +2621,60 @@ xxhash32 (Fast but less collision resistance): xxhash32 (rápido, pero con menor xxhash64 (Fastest): xxhash64 (el más rápido) "Welcome to Self-hosted LiveSync": "Bienvenido a Self-hosted LiveSync" "We will now guide you through a few questions to simplify the synchronisation setup.": - "Ahora le guiaremos con unas pocas preguntas para simplificar la configuración + "Ahora te guiaremos con unas pocas preguntas para simplificar la configuración de la sincronización。" -"First, please select the option that best describes your current situation.": "Primero, seleccione la opción que describa mejor su situación actual。" +"First, please select the option that best describes your current situation.": "Primero, seleccione la opción que mejor describa tu situación actual。" "I am setting this up for the first time": "Estoy configurando esto por primera vez" "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": - "(Seleccione esto si está configurando este dispositivo como el primer - dispositivo de sincronización). Esta opción es adecuada si es nuevo en - LiveSync y desea configurarlo desde cero。" + "(Selecciona esto si estás configurando este dispositivo como el primer + dispositivo de sincronización). Esta opción es adecuada si eres nuevo en + LiveSync y quieres configurarlo desde cero。" "I am adding a device to an existing synchronisation setup": "Estoy agregando un dispositivo a una configuración de sincronización existente" "(Select this if another device is already using LiveSync.) This option adds this device to that existing synchronisation setup.": - "(Seleccione esto si ya utiliza la sincronización en otro ordenador o - teléfono). Esta opción es adecuada si desea añadir este dispositivo a una + "(Seleccione esto si ya utilizas la sincronización en otro ordenador o + teléfono). Esta opción es adecuada si quieres añadir este dispositivo a una configuración de LiveSync existente。" "Yes, I want to set up a new synchronisation": "Sí, quiero configurar una nueva sincronización" "Yes, I want to add this device to my existing synchronisation": "Sí, quiero añadir este dispositivo a mi sincronización existente" "No, please take me back": "No, volver atrás" "Device Setup Method": "Método de configuración del dispositivo" "You are adding this device to an existing synchronisation setup.": - "Está añadiendo este dispositivo a una configuración de sincronización + "Estás añadiendo este dispositivo a una configuración de sincronización existente。" -"Please select a method to import the settings from another device.": "Seleccione un método para importar la configuración desde otro dispositivo。" -"Use a Setup URI (Recommended)": "Usar un URI de configuración (recomendado)" -"Paste the Setup URI generated from one of your active devices.": "Pegue el URI de configuración generado desde uno de sus dispositivos activos。" +"Please select a method to import the settings from another device.": "Selecciona un método para importar la configuración desde otro dispositivo。" +"Use a Setup URI (Recommended)": "Usar URI de configuración (recomendado)" +"Paste the Setup URI generated from one of your active devices.": "Pega el URI de configuración generado desde uno de tus dispositivos activos。" "Scan a QR Code (Recommended for mobile)": "Escanear un código QR (recomendado para móviles)" "Scan the QR code displayed on an active device using this device's camera.": - "Escanee con la cámara de este dispositivo el código QR mostrado en un + "Escanea con la cámara de este dispositivo el código QR mostrado en un dispositivo activo。" "Enter the server information manually": "Introducir manualmente la información del servidor" "Configure the same server information as your other devices again, manually, very advanced users only.": - "Configure manualmente la misma información del servidor que en sus otros + "Configura manualmente la misma información del servidor que en sus otros dispositivos. Solo para usuarios muy avanzados。" "Proceed with Setup URI": "Continuar con el URI de configuración" -"I know my server details, let me enter them": "Conozco los datos de mi servidor; permítame introducirlos" -"Please select an option to proceed": "Seleccione una opción para continuar" +"I know my server details, let me enter them": "Conozco los datos de mi servidor; déjame introducirlos" +"Please select an option to proceed": "Selecciona una opción para continuar" "Connection Method": "Método de conexión" "We will now proceed with the server configuration.": "Ahora continuaremos con la configuración del servidor。" -"How would you like to configure the connection to your server?": "¿Cómo desea configurar la conexión con su servidor?" +"How would you like to configure the connection to your server?": "¿Cómo quieres configurar la conexión con tu servidor?" "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": "Un URI de configuración es una única cadena de texto que contiene la dirección del servidor y los datos de autenticación. Si el script de - instalación de su servidor generó un URI, usarlo proporciona una configuración + instalación de tu servidor generó un URI, usarla proporciona una configuración sencilla y segura。" "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": "Esta es una opción avanzada para usuarios que no disponen de un URI o que desean configurar parámetros detallados。" "Enter Server Information": "Introducir información del servidor" -"Please select the type of server to which you are connecting.": "Seleccione el tipo de servidor al que se está conectando。" +"Please select the type of server to which you are connecting.": "Selecciona el tipo de servidor al que se está conectando。" "Continue to CouchDB setup": "Continuar con la configuración de CouchDB" "Continue to S3/MinIO/R2 setup": "Continuar con la configuración de S3/MinIO/R2" "Continue to Peer-to-Peer only setup": "Continuar con la configuración solo Peer-to-Peer" "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": - "Este es el método de sincronización más adecuado para el diseño. Todas las + "Este es el método de sincronización más adecuado para el diseño del complemento. Todas las funciones están disponibles. Debe tener configurada una instancia de CouchDB。" "S3/MinIO/R2 Object Storage": "Almacenamiento de objetos S3/MinIO/R2" "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": @@ -2694,4 +2694,4 @@ You should perform this operation only in exceptional circumstances, such as whe "Solo deberías realizar esta operación en circunstancias excepcionales: cuando los datos del servidor estén completamente corruptos, cuando ya no necesites los cambios de los demás dispositivos o cuando el tamaño de la base de datos - sea inusualmente grande respecto al del Vault." + sea inusualmente grande respecto a la bóveda." From 990a026e85d8948ce8e8d0aa45cba5679a01a649 Mon Sep 17 00:00:00 2001 From: speedy-axolotl <260110665+speedy-axolotl@users.noreply.github.com> Date: Sat, 26 Sep 2026 12:29:56 -0400 Subject: [PATCH 05/58] Run i18n:bake --- src/common/messages/combinedMessages.prod.ts | 206 +++++++++---------- src/common/messagesJson/es.json | 206 +++++++++---------- 2 files changed, 206 insertions(+), 206 deletions(-) diff --git a/src/common/messages/combinedMessages.prod.ts b/src/common/messages/combinedMessages.prod.ts index 0bcbaaf2..1c9da66f 100644 --- a/src/common/messages/combinedMessages.prod.ts +++ b/src/common/messages/combinedMessages.prod.ts @@ -2,7 +2,7 @@ export const allMessages: Readonly [!DETAILS]-\n> These flags are set by the plug-in while rebuilding, or fetching. If the process ends abnormally, it may be kept unintended.\n> If you are not sure, you can try to rerun these processes. Make sure to back your vault up.\n", - es: "Self-hosted LiveSync se ha configurado para ignorar algunos eventos. ¿Es esto correcto?\n\n| Tipo | Estado | Nota |\n|:---:|:---:|---|\n| Eventos de almacenamiento | ${fileWatchingStatus} | Se ignorará cada modificación |\n| Eventos de base de datos | ${parseReplicationStatus} | Cada cambio sincronizado se pospondrá |\n\n¿Quieres reanudarlos y reiniciar Obsidian?\n\n> [!DETAILS]-\n> Estas banderas son establecidas por el complemento mientras se reconstruye o se obtiene. Si el proceso termina de forma anormal, puede mantenerse sin querer.\n> Si no estás seguro, puedes intentar volver a ejecutar estos procesos. Asegúrate de hacer una copia de seguridad de tu bóveda.\n", + es: "Self-hosted LiveSync se ha configurado para ignorar algunos eventos. ¿Es esto correcto?\n\n| Tipo | Estado | Nota |\n|:---:|:---:|---|\n| Eventos de almacenamiento | ${fileWatchingStatus} | Se ignorará cada modificación |\n| Eventos de base de datos | ${parseReplicationStatus} | Cada cambio sincronizado se pospondrá |\n\n¿Quieres reanudarlos y reiniciar Obsidian?\n\n> [!DETAILS]-\n> Estas banderas son establecidas por el complemento mientras se reconstruye o se obtiene. Si el proceso termina de forma anormal, puede mantenerse sin querer.\n> Si no estás seguro puedes intentar volver a ejecutar estos procesos. Asegúrate de hacer una copia de seguridad de tu bóveda.\n", fr: "Self-hosted LiveSync a été configuré pour ignorer certains événements. Est-ce correct ?\n\n| Type | Statut | Note |\n|:---:|:---:|---|\n| Événements de stockage | ${fileWatchingStatus} | Toute modification sera ignorée |\n| Événements de base | ${parseReplicationStatus} | Tout changement synchronisé sera reporté |\n\nVoulez-vous les reprendre et redémarrer Obsidian ?\n\n> [!DETAILS]-\n> Ces indicateurs sont définis par le plug-in lors d'une reconstruction ou d'une récupération. Si le processus se termine anormalement, ils peuvent rester activés involontairement.\n> Si vous n'êtes pas certain, vous pouvez relancer ces processus. Veillez à sauvegarder votre coffre.\n", he: "Self-hosted LiveSync הוגדר להתעלם מאירועים מסוימים. האם זה נכון?\n\n| סוג | סטטוס | הערה |\n|:---:|:---:|---|\n| אירועי אחסון | ${fileWatchingStatus} | כל שינוי יתעלם |\n| אירועי מסד נתונים | ${parseReplicationStatus} | כל שינוי מסונכרן יידחה |\n\nהאם לחדש אותם ולהפעיל מחדש את Obsidian?\n\n> [!DETAILS]-\n> דגלים אלה מוגדרים על ידי הפלאגין במהלך בנייה מחדש או משיכה. אם התהליך הסתיים בצורה לא תקינה, ייתכן שהם נשארו כלא מכוון.\n> אם אינך בטוח, ניתן לנסות להריץ מחדש את התהליכים. ודא שיש לך גיבוי של הכספת.\n", ja: "Self-hosted LiveSyncは一部のイベントを無視するように設定されています。これは正しいですか?\n\n| タイプ | ステータス | メモ |\n|:---:|:---:|---|\n| ストレージイベント | ${fileWatchingStatus} | すべての変更が無視されます |\n| データベースイベント | ${parseReplicationStatus} | すべての同期された変更が延期されます |\n\nこれらを再開してObsidianを再起動しますか?\n\n> [!DETAILS]-\n> これらのフラグは、プラグインが再構築またはフェッチ中に設定されます。プロセスが異常終了した場合、意図せず保持されることがあります。\n> 不明な場合は、これらのプロセスを再実行してみてください。必ず保管庫をバックアップしてください。\n", @@ -4340,7 +4340,7 @@ export const allMessages: Readonly ${remote})": "Superior (${local} > ${remote})", "Highlight diff": "Resaltar las diferencias", "How to display network errors when the sync server is unreachable.": "Cómo mostrar los errores de red cuando el servidor de sincronización no está disponible.", - "How would you like to configure the connection to your server?": "¿Cómo desea configurar la conexión con su servidor?", + "How would you like to configure the connection to your server?": "¿Cómo quieres configurar la conexión con tu servidor?", "However, This should not be enabled if you want to increase your secrecy more.": "Sin embargo, esto no debería habilitarse si quieres aumentar aún más tu privacidad.", "I am adding a device to an existing synchronisation setup": "Estoy agregando un dispositivo a una configuración de sincronización existente", "I am setting this up for the first time": "Estoy configurando esto por primera vez", "I am setting up a new server for the first time / I want to reset my existing server.": "Estoy configurando un servidor nuevo por primera vez / quiero restablecer mi servidor actual.", - "I am unable to create a backup of my Vault.": "No puedo crear una copia de seguridad de mi Vault.", - "I am unable to create a backup of my Vaults.": "No puedo crear una copia de seguridad de mis Vaults.", - "I have created a backup of my Vault.": "He creado una copia de seguridad de mi Vault.", - "I know my server details, let me enter them": "Conozco los datos de mi servidor; permítame introducirlos", + "I am unable to create a backup of my Vault.": "No puedo crear una copia de seguridad de mi bóveda.", + "I am unable to create a backup of my Vaults.": "No puedo crear una copia de seguridad de mis bóvedas.", + "I have created a backup of my Vault.": "He creado una copia de seguridad de mi bóveda.", + "I know my server details, let me enter them": "Conozco los datos de mi servidor; déjame introducirlos", "I understand that all changes made on other smartphones or computers possibly could be lost.": "Entiendo que todos los cambios hechos en otros móviles u ordenadores podrían perderse.", "I understand that other devices will no longer be able to synchronise, and will need to be reset the synchronisation information.": "Entiendo que los demás dispositivos ya no podrán sincronizar y que habrá que restablecer su información de sincronización.", "I understand that this action is irreversible once performed.": "Entiendo que esta acción es irreversible una vez realizada.", "I understand the risks and will proceed without a backup.": "Entiendo los riesgos y continuaré sin copia de seguridad.", "I Understand, Overwrite Server": "Lo entiendo, sobrescribir el servidor", - "If \"Auto Start P2P Connection\" is enabled, the P2P connection will be started automatically when the plug-in launches.": "Si «Iniciar la conexión P2P automáticamente» está activado, la conexión P2P se iniciará al arrancar el complemento.", - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": "Si se desactiva, chunks se dividen en hilo UI (comportamiento anterior)", - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": "Habilita sincronización eficiente por archivo. Requiere migración y actualizar todos dispositivos a v0.23.18. Pierde compatibilidad con versiones antiguas", + "If \"Auto Start P2P Connection\" is enabled, the P2P connection will be started automatically when the plug-in launches.": "Si «Iniciar la conexión P2P automáticamente» está activada, la conexión P2P se iniciará al arrancar el complemento.", + "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": "Si se desactiva, los chunks se dividen en el hilo de UI (comportamiento anterior)", + "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": "Habilita sincronización eficiente por archivo. Requiere migración y actualizar todos los dispositivos a v0.23.18. Pierde compatibilidad con versiones antiguas.", "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": "Divide chunks en máximo 100 ítems. Menos eficiente en deduplicación", - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": "Chunks nuevos se mantienen temporalmente en el documento hasta estabilizarse", + "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": "Chunks nuevos se mantienen temporalmente en el documento hasta estabilizarse.", "If enabled, the ⛔ icon will be shown inside the status instead of the file warnings banner. No details will be shown.": "Si se activa, se mostrará el icono ⛔ en el estado en lugar del banner de advertencia de archivos. No se mostrarán detalles.", - "If enabled, the file under 1kb will be processed in the UI thread.": "Archivos <1kb se procesan en hilo UI", + "If enabled, the file under 1kb will be processed in the UI thread.": "Archivos <1kb se procesan en el hilo de UI", "If enabled, the notification of hidden files change will be suppressed.": "Si se habilita, se suprimirá la notificación de cambios en archivos ocultos.", "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": "Si se habilita, todos los chunks se almacenan con la revisión hecha desde su contenido. (comportamiento anterior)", "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": "Si se habilita, todos los archivos se manejan como sensibles a mayúsculas (comportamiento anterior)", - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": "Divide chunks en segmentos semánticos. No todos los sistemas lo soportan", - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": "Saltar cambios en archivos locales que coincidan con ignore files. Cambios remotos usan ignore files locales", - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": "Mantiene conexión 60s. Si no hay cambios, reinicia socket. Útil con proxies limitantes", + "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": "Divide chunks en segmentos semánticos. No todos los sistemas lo soportan.", + "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": "Saltar cambios en archivos locales que coincidan con ignore files. Cambios remotos usan ignore files locales.", + "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": "Mantiene una conexión abierta por 60s en CouchDB. Si no hay cambios, se reinicia socket en vez de dejarlo abierto de forma indefinida. Útil con proxies limitantes pero puede aumentar el uso de recursos.", "If you cannot avoid CORS issues, you might want to try this option. It uses Obsidian's internal API to communicate with the CouchDB server. Not compliant with web standards, but works. Note that this might break in future Obsidian versions.": "Si no puedes evitar problemas de CORS, prueba esta opción. Usa la API interna de Obsidian para comunicarse con el servidor CouchDB. No cumple los estándares web, pero funciona. Ten en cuenta que podría dejar de funcionar en versiones futuras de Obsidian.", "If you cannot avoid CORS issues, you might want to try this option. It uses Obsidian's internal API to communicate with the S3 server. Not compliant with web standards, but works. Note that this might break in future Obsidian versions.": "Si no puedes evitar problemas de CORS, prueba esta opción. Usa la API interna de Obsidian para comunicarse con el servidor S3. No cumple los estándares web, pero funciona. Ten en cuenta que podría dejar de funcionar en versiones futuras de Obsidian.", - "If you have unsynchronised changes in your Vault on this device, they will likely diverge from the server's versions after the reset. This may result in a large number of file conflicts.": "Si tienes cambios sin sincronizar en el Vault de este dispositivo, es probable que divergan de las versiones del servidor tras el restablecimiento. Esto puede provocar un gran número de conflictos de archivos.", - "If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value.": "Si alcanzas el límite de tamaño de carga al usar IBM Cloudant, reduce el tamaño de lote y el límite de lote.", + "If you have unsynchronised changes in your Vault on this device, they will likely diverge from the server's versions after the reset. This may result in a large number of file conflicts.": "Si tienes cambios sin sincronizar en la bóveda de este dispositivo, es probable que diverjan de las versiones del servidor tras el restablecimiento. Esto puede provocar un gran número de conflictos de archivos.", + "If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value.": "Si alcanzas el límite de tamaño de carga al usar IBM Cloudant, por favor reduce el tamaño de lote y el límite de lote.", "If you understand the risks and still wish to proceed, select so.": "Si entiendes los riesgos y aun así quieres continuar, indícalo.", "If you want to store the data in a specific folder within the bucket, you can specify a folder prefix here. Otherwise, leave it blank to store data at the root of the bucket.": "Si quieres guardar los datos en una carpeta concreta del bucket, indica aquí un prefijo de carpeta. Si no, déjalo vacío para guardarlos en la raíz del bucket.", "If you want to use `LiveSync`, you should broadcast changes. All `watching` peers which detects this will start the replication for fetching.": "Si quieres usar `LiveSync`, debes difundir los cambios. Todos los pares que estén `observando` y lo detecten iniciarán la replicación para obtenerlos.", @@ -353,7 +353,7 @@ "Ignore patterns": "Patrones de exclusión", "Import connection": "Importar conexión", "IMPORTANT": "IMPORTANTE", - "In most cases, you should stick with the default algorithm (${algorithm}), This setting is only required if you have an existing Vault encrypted in a different format.": "En la mayoría de los casos deberías mantener el algoritmo predeterminado (${algorithm}). Este ajuste solo es necesario si ya tienes un Vault cifrado en otro formato.", + "In most cases, you should stick with the default algorithm (${algorithm}), This setting is only required if you have an existing Vault encrypted in a different format.": "En la mayoría de los casos deberías mantener el algoritmo predeterminado (${algorithm}). Este ajuste solo es necesario si ya tienes una bóveda cifrada en otro formato.", "In this scenario, Self-hosted LiveSync will recreate metadata for every file and deliberately generate conflicts. Where the file content is identical, these conflicts will be resolved automatically.": "En este caso, Self-hosted LiveSync recreará los metadatos de todos los archivos y generará conflictos a propósito. Cuando el contenido del archivo sea idéntico, esos conflictos se resolverán automáticamente.", "Incoming:": "Entrantes:", "Incubate Chunks in Document": "Incubar chunks en documento", @@ -470,16 +470,16 @@ "moduleInputUIObsidian.defaultTitleSelect": "Seleccionar", "moduleInputUIObsidian.optionNo": "No", "moduleInputUIObsidian.optionYes": "Sí", - "moduleLiveSyncMain.logAdditionalSafetyScan": "Escanéo de seguridad adicional...", + "moduleLiveSyncMain.logAdditionalSafetyScan": "Escaneo de seguridad adicional...", "moduleLiveSyncMain.logLoadingPlugin": "Cargando complemento...", "moduleLiveSyncMain.logPluginInitCancelled": "La inicialización del complemento fue cancelada por un módulo", "moduleLiveSyncMain.logPluginVersion": "Self-hosted LiveSync v${manifestVersion} ${packageVersion}", "moduleLiveSyncMain.logReadChangelog": "LiveSync se ha actualizado, ¡por favor lee el registro de cambios!", - "moduleLiveSyncMain.logSafetyScanCompleted": "Escanéo de seguridad adicional completado", + "moduleLiveSyncMain.logSafetyScanCompleted": "Escaneo de seguridad adicional completado", "moduleLiveSyncMain.logSafetyScanFailed": "El escaneo de seguridad adicional ha fallado en un módulo", "moduleLiveSyncMain.logUnloadingPlugin": "Descargando complemento...", "moduleLiveSyncMain.logVersionUpdate": "LiveSync se ha actualizado, en caso de actualizaciones que rompan, toda la sincronización automática se ha desactivado temporalmente. Asegúrate de que todos los dispositivos estén actualizados antes de habilitar.", - "moduleLiveSyncMain.msgScramEnabled": "Self-hosted LiveSync se ha configurado para ignorar algunos eventos. ¿Es esto correcto?\n\n| Tipo | Estado | Nota |\n|:---:|:---:|---|\n| Eventos de almacenamiento | ${fileWatchingStatus} | Se ignorará cada modificación |\n| Eventos de base de datos | ${parseReplicationStatus} | Cada cambio sincronizado se pospondrá |\n\n¿Quieres reanudarlos y reiniciar Obsidian?\n\n> [!DETAILS]-\n> Estas banderas son establecidas por el complemento mientras se reconstruye o se obtiene. Si el proceso termina de forma anormal, puede mantenerse sin querer.\n> Si no estás seguro, puedes intentar volver a ejecutar estos procesos. Asegúrate de hacer una copia de seguridad de tu bóveda.\n", + "moduleLiveSyncMain.msgScramEnabled": "Self-hosted LiveSync se ha configurado para ignorar algunos eventos. ¿Es esto correcto?\n\n| Tipo | Estado | Nota |\n|:---:|:---:|---|\n| Eventos de almacenamiento | ${fileWatchingStatus} | Se ignorará cada modificación |\n| Eventos de base de datos | ${parseReplicationStatus} | Cada cambio sincronizado se pospondrá |\n\n¿Quieres reanudarlos y reiniciar Obsidian?\n\n> [!DETAILS]-\n> Estas banderas son establecidas por el complemento mientras se reconstruye o se obtiene. Si el proceso termina de forma anormal, puede mantenerse sin querer.\n> Si no estás seguro puedes intentar volver a ejecutar estos procesos. Asegúrate de hacer una copia de seguridad de tu bóveda.\n", "moduleLiveSyncMain.optionKeepLiveSyncDisabled": "Mantener LiveSync desactivado", "moduleLiveSyncMain.optionResumeAndRestart": "Reanudar y reiniciar Obsidian", "moduleLiveSyncMain.titleScramEnabled": "Scram habilitado", @@ -495,7 +495,7 @@ "moduleMigration.insecureChunkExist.buttons.later": "Lo haré más tarde", "moduleMigration.insecureChunkExist.buttons.rebuild": "Reconstruir todo", "moduleMigration.insecureChunkExist.laterMessage": "¡Te recomendamos encarecidamente solucionarlo cuanto antes!", - "moduleMigration.insecureChunkExist.message": "Algunos chunks no se almacenan de forma segura y no están cifrados en las bases de datos.\n**Reconstruye la base de datos para solucionarlo**.\n\nSi tu base de datos remota no está configurada con SSL o usa credenciales poco seguras, **corres el riesgo de exponer datos sensibles**.\n\nNota: actualiza Self-hosted LiveSync a la v0.25.6 o superior en todos tus dispositivos y haz una copia de seguridad fiable de tu vault.\nNota 2: reconstruir todo y obtener los datos consume algo de tiempo y de tráfico; hazlo en horas de poco uso y con una conexión de red estable.\n", + "moduleMigration.insecureChunkExist.message": "Algunos chunks no se almacenan de forma segura y no están cifrados en las bases de datos.\n**Reconstruye la base de datos para solucionarlo**.\n\nSi tu base de datos remota no está configurada con SSL o usa credenciales poco seguras, **corres el riesgo de exponer datos sensibles**.\n\nNota: actualiza Self-hosted LiveSync a la v0.25.6 o superior en todos tus dispositivos y haz una copia de seguridad fiable de tu bóveda.\nNota 2: reconstruir todo y obtener los datos consume algo de tiempo y de tráfico; hazlo en horas de poco uso y con una conexión de red estable.\n", "moduleMigration.insecureChunkExist.title": "¡Se han encontrado chunks no seguros!", "moduleMigration.logBulkSendCorrupted": "El envío de fragmentos en bloque se ha habilitado, sin embargo, esta función se ha corrompido. Disculpe las molestias. Deshabilitado automáticamente.", "moduleMigration.logFetchRemoteTweakFailed": "Error al obtener los valores de ajuste remoto", @@ -626,7 +626,7 @@ "obsidianLiveSyncSettingTab.msgEnableCorsChttpd": "Establecer chttpd.enable_cors", "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": "Recomendamos habilitar el cifrado de extremo a extremo y la obfuscación de ruta. ¿Estás seguro de querer continuar sin cifrado?", "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": "¿Quieres obtener la configuración del servidor remoto?", - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": "Si la configuración del servidor no es persistente (por ejemplo, ejecutándose en docker), los valores aquí pueden cambiar. Una vez que puedas conectarte, por favor actualiza las configuraciones en el local.ini del servidor.", + "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": "Si la configuración del servidor no es persistente (por ejemplo, ejecutándose en Docker), los valores aquí pueden cambiar. Una vez que puedas conectarte, por favor actualiza las configuraciones en el local.ini del servidor.", "obsidianLiveSyncSettingTab.msgInvalidPassphrase": "Tu frase de contraseña de cifrado podría ser inválida. ¿Estás seguro de querer continuar?", "obsidianLiveSyncSettingTab.msgNewVersionNote": "¿Aquí debido a una notificación de actualización? Por favor, revise el historial de versiones. Si está satisfecho, haga clic en el botón. Una nueva actualización volverá a mostrar esto.", "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": "Configurado como URI que no es HTTPS. Ten en cuenta que esto puede no funcionar en dispositivos móviles.", @@ -659,15 +659,15 @@ "obsidianLiveSyncSettingTab.okAdminPrivileges": "✔ Tienes privilegios de administrador.", "obsidianLiveSyncSettingTab.okCorsCredentials": "✔ cors.credentials está correcto.", "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": "CORS credenciales OK", - "obsidianLiveSyncSettingTab.okCorsOriginMatched": "✔ Origen de CORS correcto", - "obsidianLiveSyncSettingTab.okCorsOrigins": "✔ cors.origins está correcto.", - "obsidianLiveSyncSettingTab.okEnableCors": "✔ httpd.enable_cors está correcto.", + "obsidianLiveSyncSettingTab.okCorsOriginMatched": "✔ Origen de CORS es correcto", + "obsidianLiveSyncSettingTab.okCorsOrigins": "✔ cors.origins es correcto.", + "obsidianLiveSyncSettingTab.okEnableCors": "✔ httpd.enable_cors es correcto.", "obsidianLiveSyncSettingTab.okEnableCorsChttpd": "✔ chttpd.enable_cors es correcto.", - "obsidianLiveSyncSettingTab.okMaxDocumentSize": "✔ couchdb.max_document_size está correcto.", - "obsidianLiveSyncSettingTab.okMaxRequestSize": "✔ chttpd.max_http_request_size está correcto.", - "obsidianLiveSyncSettingTab.okRequireValidUser": "✔ chttpd.require_valid_user está correcto.", - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": "✔ chttpd_auth.require_valid_user está correcto.", - "obsidianLiveSyncSettingTab.okWwwAuth": "✔ httpd.WWW-Authenticate está correcto.", + "obsidianLiveSyncSettingTab.okMaxDocumentSize": "✔ couchdb.max_document_size es correcto.", + "obsidianLiveSyncSettingTab.okMaxRequestSize": "✔ chttpd.max_http_request_size es correcto.", + "obsidianLiveSyncSettingTab.okRequireValidUser": "✔ chttpd.require_valid_user es correcto.", + "obsidianLiveSyncSettingTab.okRequireValidUserAuth": "✔ chttpd_auth.require_valid_user es correcto.", + "obsidianLiveSyncSettingTab.okWwwAuth": "✔ httpd.WWW-Authenticate es correcto.", "obsidianLiveSyncSettingTab.optionApply": "Aplicar", "obsidianLiveSyncSettingTab.optionCancel": "Cancelar", "obsidianLiveSyncSettingTab.optionCouchDB": "CouchDB", @@ -680,7 +680,7 @@ "obsidianLiveSyncSettingTab.optionOnEvents": "En eventos", "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": "Periódico y en eventos", "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": "Periódico con lote", - "obsidianLiveSyncSettingTab.optionRebuildBoth": "Reconstructuir ambos desde este dispositivo", + "obsidianLiveSyncSettingTab.optionRebuildBoth": "Reconstruir ambos desde este dispositivo", "obsidianLiveSyncSettingTab.optionSaveOnlySettings": "(Peligro) Guardar solo configuración", "obsidianLiveSyncSettingTab.panelChangeLog": "Registro de cambios", "obsidianLiveSyncSettingTab.panelGeneralSettings": "Configuraciones generales", @@ -726,7 +726,7 @@ "On": "Activado", "On the source device, from the command palette, run the 'Show settings as a QR code' command.": "En el dispositivo de origen, ejecuta desde la paleta de comandos «Show settings as a QR code».", "On the source device, open Obsidian.": "En el dispositivo de origen, abre Obsidian.", - "On this device, please keep this Vault open.": "En este dispositivo, mantén este Vault abierto.", + "On this device, please keep this Vault open.": "En este dispositivo, mantén esta bóveda abierta.", "On this device, switch to the camera app or use a QR code scanner to scan the displayed QR code.": "En este dispositivo, cambia a la aplicación de cámara o usa un lector de QR para escanear el código mostrado.", "Open": "Abrir", "Open connection": "Abrir conexión", @@ -746,7 +746,7 @@ "P2P.FailedToOpen": "No se pudo abrir la conexión P2P con el servidor de señalización.", "P2P.NoAutoSyncPeers": "No se han encontrado pares de sincronización automática. Configúralos en el panel de %{long_p2p_sync}.", "P2P.NoKnownPeers": "No se ha detectado ningún par; esperando a que se conecten otros...", - "P2P.Note.description": " Este replicador permite sincronizar el vault con otros dispositivos\nmediante una conexión punto a punto. Así se puede sincronizar con nuestros otros dispositivos sin usar un servicio en la nube.\nEl replicador se basa en Trystero. Usa además un servidor de señalización para establecer la conexión entre dispositivos. Ese servidor sirve para intercambiar la información de conexión y no conoce (ni debería almacenar) ninguno de nuestros datos.\n\nCualquiera puede alojar el servidor de señalización: es simplemente un relé Nostr. Por comodidad y para poder comprobar el comportamiento del replicador, vrtmrz aloja una instancia. Puedes usar ese servidor experimental o cualquier otro.\n\nPor cierto, aunque el servidor de señalización no almacene nuestros datos, sí puede ver la información de conexión de algunos de nuestros dispositivos. Tenlo en cuenta y ten precaución al usar un servidor de terceros.", + "P2P.Note.description": " Este replicador permite sincronizar la bóveda con otros dispositivos\nmediante una conexión punto a punto. Así se puede sincronizar con nuestros otros dispositivos sin usar un servicio en la nube.\nEl replicador se basa en Trystero. Usa además un servidor de señalización para establecer la conexión entre dispositivos. Ese servidor sirve para intercambiar la información de conexión y no conoce (ni debería almacenar) ninguno de nuestros datos.\n\nCualquiera puede alojar el servidor de señalización: es simplemente un relé Nostr. Por comodidad y para poder comprobar el comportamiento del replicador, vrtmrz aloja una instancia. Puedes usar ese servidor experimental o cualquier otro.\n\nPor cierto, aunque el servidor de señalización no almacene nuestros datos, sí puede ver la información de conexión de algunos de nuestros dispositivos. Tenlo en cuenta y ten precaución al usar un servidor de terceros.", "P2P.Note.important_note": "Replicador punto a punto.", "P2P.Note.important_note_sub": "Esta función sigue siendo muy experimental. Asegúrate de tener una copia de seguridad de tus datos antes de usarla. Y nos alegraría mucho que quisieras contribuir a su desarrollo.", "P2P.Note.Summary": "¿Qué es esta función? (incluye notas importantes; léelas al menos una vez)", @@ -768,7 +768,7 @@ "password": "contraseña", "Password": "Contraseña", "Paste a connection string": "Pegar cadena de conexión", - "Paste the Setup URI generated from one of your active devices.": "Pegue el URI de configuración generado desde uno de sus dispositivos activos。", + "Paste the Setup URI generated from one of your active devices.": "Pega el URI de configuración generado desde uno de tus dispositivos activos。", "Path": "Ruta", "Path Obfuscation": "Ofuscación de rutas", "Patterns to match files for overwriting instead of merging": "Patrones para identificar archivos que se sobrescribirán en lugar de fusionarse", @@ -794,16 +794,16 @@ "Please enter the CouchDB server information below.": "Introduce a continuación los datos del servidor CouchDB.", "Please enter the details required to connect to your S3/MinIO/R2 compatible object storage service.": "Introduce los datos necesarios para conectarte a tu servicio de almacenamiento de objetos compatible con S3/MinIO/R2.", "Please enter the Peer-to-Peer Synchronisation information below.": "Introduce a continuación los datos de la sincronización punto a punto.", - "Please enter the Setup URI that was generated during server installation or on another device, along with the vault passphrase.": "Introduce el Setup URI generado durante la instalación del servidor o en otro dispositivo, junto con la frase de contraseña del vault.", + "Please enter the Setup URI that was generated during server installation or on another device, along with the vault passphrase.": "Introduce el Setup URI generado durante la instalación del servidor o en otro dispositivo, junto con la frase de contraseña de la bóveda.", "Please follow the steps below to import settings from your existing device.": "Sigue los pasos siguientes para importar los ajustes desde tu dispositivo actual.", "PLEASE NOTE": "TEN EN CUENTA", "Please select 'Cancel' explicitly to cancel this operation.": "Selecciona «Cancelar» de forma explícita para cancelar esta operación.", - "Please select a method to import the settings from another device.": "Seleccione un método para importar la configuración desde otro dispositivo。", + "Please select a method to import the settings from another device.": "Selecciona un método para importar la configuración desde otro dispositivo。", "Please select an active P2P remote configuration to change P2P sync targets.": "Selecciona una configuración remota P2P activa para cambiar los destinos de sincronización P2P.", - "Please select an option to proceed": "Seleccione una opción para continuar", + "Please select an option to proceed": "Selecciona una opción para continuar", "Please select the button below to restart and proceed to the data fetching confirmation.": "Pulsa el botón de abajo para reiniciar y pasar a la confirmación de la obtención de datos.", "Please select the button below to restart and proceed to the final confirmation.": "Pulsa el botón de abajo para reiniciar y pasar a la confirmación final.", - "Please select the type of server to which you are connecting.": "Seleccione el tipo de servidor al que se está conectando。", + "Please select the type of server to which you are connecting.": "Selecciona el tipo de servidor al que se está conectando。", "Please select your situation.": "Selecciona tu situación.", "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": "Define un nombre para identificar este dispositivo. Debe ser único entre tus dispositivos. Mientras no esté configurado, no podremos habilitar esta función.", "Please set this device name": "Define el nombre de este dispositivo", @@ -840,7 +840,7 @@ "RedFlag.FetchRemoteConfig.Message": "¿Quieres obtener y aplicar en este dispositivo los ajustes de preferencias guardados en el remoto?", "RedFlag.FetchRemoteConfig.Title": "Obtener la configuración remota", "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.": "Reduce el espacio de almacenamiento descartando todas las revisiones que no sean la última. Requiere la misma cantidad de espacio libre en el servidor remoto y en el cliente local.", - "Reducing the frequency with which on-disk changes are reflected into the DB": "Reducir frecuencia de actualizaciones de disco a BD", + "Reducing the frequency with which on-disk changes are reflected into the DB": "Reducir frecuencia de actualizaciones de disco a base de datos", "Refresh": "Actualizar", "Region": "Región", "Relay settings": "Ajustes del relé", @@ -870,7 +870,7 @@ "Replicator.Message.SomeModuleFailed": "La replicación se ha cancelado por el fallo de algún módulo", "Replicator.Message.VersionUpFlash": "Se ha detectado una actualización. Abre el diálogo de ajustes y consulta el registro de cambios. La replicación se ha cancelado.", "Requires restart of Obsidian": "Requiere reiniciar Obsidian", - "Requires restart of Obsidian.": "Requiere reiniciar Obsidian", + "Requires restart of Obsidian.": "Requiere reiniciar Obsidian.", "Rerun Onboarding Wizard": "Volver a ejecutar el asistente de configuración inicial", "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": "Vuelve a ejecutar el asistente de configuración inicial para configurar Self-hosted LiveSync de nuevo.", "Rerun Wizard": "Volver a ejecutar el asistente", @@ -923,19 +923,19 @@ "Scan for hidden files before replication": "Escanear archivos ocultos antes de replicar", "Scan hidden files periodically": "Escanear archivos ocultos periódicamente", "Scan QR Code": "Escanear código QR", - "Scan the QR code displayed on an active device using this device's camera.": "Escanee con la cámara de este dispositivo el código QR mostrado en un dispositivo activo。", + "Scan the QR code displayed on an active device using this device's camera.": "Escanea con la cámara de este dispositivo el código QR mostrado en un dispositivo activo。", "Schedule and Restart": "Programar y reiniciar", "Scram Switches": "Interruptores de emergencia", "Scram!": "Medidas de emergencia", "Seconds, 0 to disable": "Segundos, 0 para desactivar", - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": "Segundos. Guardado en BD local se retrasará hasta este valor tras dejar de escribir/guardar", + "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": "Segundos. Guardado en base de datos local se retrasará hasta este valor tras dejar de escribir o guardar.", "Secret Access Key": "Clave de acceso secreta", "Secret Key": "Clave secreta", "Select active P2P remote": "Seleccionar el remoto P2P activo", "Select All Shiny": "Seleccionar todo lo nuevo", "Select Flagged Shiny": "Seleccionar lo nuevo marcado", "Select P2P remote...": "Seleccionar remoto P2P...", - "Select the database adapter to use.": "Selecciona el adaptador de base de datos que se usará.", + "Select the database adapter to use.": "Selecciona el adaptador de base de datos.", "Selecting this option will result in the current data on this device being used to initialise the server. Any existing data on the server will be completely overwritten.": "Al elegir esta opción, los datos actuales de este dispositivo se usarán para inicializar el servidor. Cualquier dato existente en el servidor se sobrescribirá por completo.", "Selecting this option will result in this device joining the existing server. You need to fetching the existing synchronisation data from the server to this device.": "Al elegir esta opción, este dispositivo se unirá al servidor existente. Tendrás que obtener del servidor los datos de sincronización ya existentes.", "Selective": "Selectivo", @@ -1012,7 +1012,7 @@ "Setup.RemoteE2EE.AlgorithmWarning": "Cambiar el algoritmo de cifrado impedirá el acceso a cualquier dato cifrado anteriormente con otro algoritmo. Asegúrate de que todos tus dispositivos estén configurados para usar el mismo algoritmo y así mantener el acceso a tus datos.", "Setup.RemoteE2EE.ButtonCancel": "Cancelar", "Setup.RemoteE2EE.ButtonProceed": "Continuar", - "Setup.RemoteE2EE.DefaultAlgorithmDesc": "En la mayoría de los casos, debes mantener el algoritmo predeterminado (${algorithm}). Este ajuste solo es necesario si ya tienes un Vault cifrado con un formato diferente.", + "Setup.RemoteE2EE.DefaultAlgorithmDesc": "En la mayoría de los casos, debes mantener el algoritmo predeterminado (${algorithm}). Este ajuste solo es necesario si ya tienes una bóveda cifrada con un formato diferente.", "Setup.RemoteE2EE.Guidance": "Configura tus ajustes de cifrado de extremo a extremo.", "Setup.RemoteE2EE.LabelEncrypt": "Cifrado de extremo a extremo", "Setup.RemoteE2EE.LabelEncryptionAlgorithm": "Algoritmo de cifrado", @@ -1028,7 +1028,7 @@ "Setup.RemoteE2EE.Title": "Cifrado de extremo a extremo", "Setup.ScanQRCode.ButtonClose": "Cerrar este diálogo", "Setup.ScanQRCode.Guidance": "Sigue los pasos de abajo para importar los ajustes desde tu dispositivo actual.", - "Setup.ScanQRCode.Step1": "En este dispositivo, mantén este Vault abierto.", + "Setup.ScanQRCode.Step1": "En este dispositivo, mantén esta bóveda abierta.", "Setup.ScanQRCode.Step2": "En el dispositivo de origen, abre Obsidian.", "Setup.ScanQRCode.Step3": "En el dispositivo de origen, ejecuta desde la paleta de comandos la orden \"Mostrar ajustes como código QR\".", "Setup.ScanQRCode.Step4": "En este dispositivo, cambia a la cámara o usa un escáner QR para escanear el código mostrado.", @@ -1044,13 +1044,13 @@ "Setup.UseSetupURI.ButtonCancel": "Cancelar", "Setup.UseSetupURI.ButtonProceed": "Probar ajustes y continuar", "Setup.UseSetupURI.ErrorFailedToParse": "No se pudo procesar la URI de configuración. Revisa la URI y la frase de contraseña.", - "Setup.UseSetupURI.ErrorPassphraseRequired": "Introduce la frase de contraseña del Vault.", - "Setup.UseSetupURI.GuidanceLine1": "Introduce la URI de configuración que se generó durante la instalación del servidor o en otro dispositivo, junto con la frase de contraseña del Vault.", + "Setup.UseSetupURI.ErrorPassphraseRequired": "Introduce la frase de contraseña de la bóveda.", + "Setup.UseSetupURI.GuidanceLine1": "Introduce la URI de configuración que se generó durante la instalación del servidor o en otro dispositivo, junto con la frase de contraseña de la bóveda.", "Setup.UseSetupURI.GuidanceLine2": "Ten en cuenta que puedes generar una nueva URI de configuración ejecutando el comando \"Copiar ajustes como nueva URI de configuración\" desde la paleta de comandos.", "Setup.UseSetupURI.InvalidInfo": "La URI de configuración no es válida. Revísala e inténtalo de nuevo.", - "Setup.UseSetupURI.LabelPassphrase": "Frase de contraseña del Vault", + "Setup.UseSetupURI.LabelPassphrase": "Frase de contraseña de la bóveda", "Setup.UseSetupURI.LabelSetupURI": "URI de configuración", - "Setup.UseSetupURI.PlaceholderPassphrase": "Introduce la frase de contraseña del Vault", + "Setup.UseSetupURI.PlaceholderPassphrase": "Introduce la frase de contraseña de la bóveda", "Setup.UseSetupURI.Title": "Introducir URI de configuración", "Setup.UseSetupURI.ValidInfo": "La URI de configuración es válida y está lista para usarse.", "Should we keep folders that don't have any files inside?": "¿Mantener carpetas vacías?", @@ -1080,7 +1080,7 @@ "Stats": "Estadísticas", "Stop ⚡": "Detener ⚡", "Stop Broadcasting": "Detener difusión", - "Stop reflecting database changes to storage files.": "Dejar de reflejar cambios de BD en archivos", + "Stop reflecting database changes to storage files.": "Dejar de reflejar cambios de base de datos en archivos", "Stop watching for file changes.": "Dejar de monitorear cambios en archivos", "Storage -> Database": "Almacenamiento -> Base de datos", "Strongly Recommended": "Muy recomendado", @@ -1108,7 +1108,7 @@ "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": "Solo pruebas - Resolver conflictos sincronizando copias nuevas (puede sobrescribir modificaciones)", "The connection to the server has been configured successfully. As the next step,": "La conexión con el servidor se ha configurado correctamente. Como paso siguiente,", "The delay for consecutive on-demand fetches": "Retraso entre obtenciones consecutivas", - "The files in this Vault are almost identical to the server's.": "Los archivos de este Vault son casi idénticos a los del servidor.", + "The files in this Vault are almost identical to the server's.": "Los archivos de esta bóveda son casi idénticos a los del servidor.", "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": "A los siguientes nodos aceptados les falta su información de nodo:\n- ${missingNodes}\n\nEsto indica que llevan tiempo sin conectarse o que se han quedado en una versión antigua.\nSi es posible, conviene actualizar todos los dispositivos. Si tienes dispositivos que ya no usas, puedes borrar todos los nodos aceptados bloqueando el remoto una vez.", "The Group ID and passphrase are used to identify your group of devices. Make sure to use the same Group ID and passphrase on all devices you want to synchronise.": "El ID de grupo y la frase de contraseña identifican tu grupo de dispositivos. Usa el mismo ID de grupo y la misma frase de contraseña en todos los dispositivos que quieras sincronizar.", "The Hash algorithm for chunk IDs": "Algoritmo hash para IDs de chunks", @@ -1126,7 +1126,7 @@ "the synchronisation data on the server will be built based on the current data on this device.": "los datos de sincronización del servidor se construirán a partir de los datos actuales de este dispositivo.", "Themes": "Temas", "There is a way to resolve this on other devices.": "Hay una forma de resolver esto en los demás dispositivos.", - "There may be differences between the files in this Vault and the server.": "Puede haber diferencias entre los archivos de este Vault y los del servidor.", + "There may be differences between the files in this Vault and the server.": "Puede haber diferencias entre los archivos de esta bóveda y los del servidor.", "Therefore, we ask that you exercise extreme caution when configuring server information manually. If an incorrect passphrase is entered, the data on the server will become corrupted.": "Por tanto, extrema la precaución al configurar manualmente los datos del servidor. Si introduces una frase de contraseña incorrecta, los datos del servidor se corromperán.", "This can isolate your connections between devices. Use the same Room ID for the same devices.": "Esto permite aislar tus conexiones entre dispositivos. Usa el mismo ID de sala para los mismos dispositivos.", "This device": "Este dispositivo", @@ -1134,14 +1134,14 @@ "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": "Esta función permite la sincronización directa entre dispositivos. No requiere servidor, pero ambos dispositivos deben estar en línea al mismo tiempo para que la sincronización se produzca, y algunas funciones pueden ser limitadas. La conexión a Internet solo se necesita para la señalización (detección de pares), no para la transferencia de datos。", "This first setup has several short steps because it confirms encryption, the connection method, and which device provides the initial data. Once it is complete, additional devices can reuse a Setup URI.": "Esta primera configuración consta de varios pasos breves, ya que confirma el cifrado, el método de conexión y qué dispositivo aporta los datos iniciales. Una vez completada, los demás dispositivos podrán reutilizar un Setup URI.", "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": "Esta es una opción avanzada para usuarios que no disponen de un URI o que desean configurar parámetros detallados。", - "This is an extremely powerful operation. We strongly recommend that you copy your Vault folder to a safe location.": "Esta es una operación extremadamente potente. Te recomendamos encarecidamente copiar la carpeta de tu Vault a un lugar seguro.", - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": "Este es el método de sincronización más adecuado para el diseño. Todas las funciones están disponibles. Debe tener configurada una instancia de CouchDB。", + "This is an extremely powerful operation. We strongly recommend that you copy your Vault folder to a safe location.": "Esta es una operación extremadamente potente. Te recomendamos encarecidamente copiar la carpeta de tu bóveda a un lugar seguro.", + "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": "Este es el método de sincronización más adecuado para el diseño del complemento. Todas las funciones están disponibles. Debe tener configurada una instancia de CouchDB。", "This optional check uses Obsidian's internal request API and sends the credentials above to the CouchDB server. Use it only with a server you trust; administrator access may be required.": "Esta comprobación opcional usa la API interna de solicitudes de Obsidian y envía las credenciales anteriores al servidor CouchDB. Utilízala solo con un servidor de confianza; puede requerir acceso de administrador.", - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": "Esta frase no se copia a otros dispositivos. Usará `Default` hasta reconfigurar", + "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": "Esta frase no se copia a otros dispositivos. Usará `Default` hasta que lo configures de nuevo.", "This password is used to encrypt the connection. Use something long enough.": "Esta contraseña se usa para cifrar la conexión. Usa algo suficientemente largo.", - "This procedure will first delete all existing synchronisation data from the server. Following this, the server data will be completely rebuilt, using the current state of your Vault on this device (including its local database) as": "Este procedimiento eliminará primero todos los datos de sincronización existentes en el servidor. A continuación, los datos del servidor se reconstruirán por completo usando el estado actual del Vault de este dispositivo (incluida su base de datos local) como", + "This procedure will first delete all existing synchronisation data from the server. Following this, the server data will be completely rebuilt, using the current state of your Vault on this device (including its local database) as": "Este procedimiento eliminará primero todos los datos de sincronización existentes en el servidor. A continuación, los datos del servidor se reconstruirán por completo usando el estado actual de la bóveda de este dispositivo (incluida su base de datos local) como", "This setting must be the same even when connecting to multiple synchronisation destinations.": "Este ajuste debe ser el mismo incluso si te conectas a varios destinos de sincronización.", - "This Vault is empty, or contains only new files that are not on the server.": "Este Vault está vacío o solo contiene archivos nuevos que no están en el servidor.", + "This Vault is empty, or contains only new files that are not on the server.": "Esta bóveda está vacía o solo contiene archivos nuevos que no están en el servidor.", "This will rebuild the local database on this device using the most recent data from the server. This action is designed to resolve synchronisation inconsistencies and restore correct functionality.": "Esto reconstruirá la base de datos local de este dispositivo con los datos más recientes del servidor. La acción está pensada para resolver inconsistencias de sincronización y restaurar el funcionamiento correcto.", "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": "Esto recreará los fragmentos de todos los archivos. Si faltaban fragmentos, esto puede corregir los errores.", "To minimise the creation of new conflicts": "Para minimizar la creación de nuevos conflictos", @@ -1273,9 +1273,9 @@ "Ui.Settings.Maintenance.SendChunks": "Enviar los chunks", "Ui.Settings.Maintenance.Syncing": "Sincronización", "Ui.Settings.Maintenance.WarningLockedReadyAction": "Estoy listo, desbloquear la base de datos", - "Ui.Settings.Maintenance.WarningLockedReadyText": "Para evitar que el vault se corrompa, la base de datos remota se ha bloqueado para la sincronización. (Este dispositivo está marcado como «resuelto».) Cuando todos tus dispositivos estén marcados como «resueltos», desbloquea la base de datos. Este aviso seguirá apareciendo hasta que la replicación confirme que el dispositivo está resuelto.", + "Ui.Settings.Maintenance.WarningLockedReadyText": "Para evitar que la bóveda se corrompa, la base de datos remota se ha bloqueado para la sincronización. (Este dispositivo está marcado como «resuelto».) Cuando todos tus dispositivos estén marcados como «resueltos», desbloquea la base de datos. Este aviso seguirá apareciendo hasta que la replicación confirme que el dispositivo está resuelto.", "Ui.Settings.Maintenance.WarningLockedResolveAction": "He hecho una copia de seguridad, marcar este dispositivo como resuelto", - "Ui.Settings.Maintenance.WarningLockedResolveText": "La base de datos remota está bloqueada para la sincronización a fin de evitar que el vault se corrompa, porque este dispositivo no está marcado como «resuelto». Haz una copia de seguridad de tu vault, restablece la base de datos local y selecciona «Marcar este dispositivo como resuelto». Este aviso seguirá apareciendo hasta que la replicación confirme que el dispositivo está resuelto.", + "Ui.Settings.Maintenance.WarningLockedResolveText": "La base de datos remota está bloqueada para la sincronización a fin de evitar que la bóveda se corrompa, porque este dispositivo no está marcado como «resuelto». Haz una copia de seguridad de tu bóveda, restablece la base de datos local y selecciona «Marcar este dispositivo como resuelto». Este aviso seguirá apareciendo hasta que la replicación confirme que el dispositivo está resuelto.", "Ui.Settings.Maintenance.WriteRedFlagAndRestart": "Marcar y reiniciar", "Ui.Settings.Patches.CompatibilityConflict": "Compatibilidad (comportamiento ante conflictos)", "Ui.Settings.Patches.CompatibilityDatabase": "Compatibilidad (estructura de la base de datos)", @@ -1304,7 +1304,7 @@ "Ui.Settings.Patches.RemediationNoLimit": "Sin límite configurado", "Ui.Settings.Patches.RemediationRestarting": "Se ha cambiado el ajuste de remediación. Reiniciando Obsidian...", "Ui.Settings.Patches.RemediationRestartLater": "Más tarde", - "Ui.Settings.Patches.RemediationRestartMessage": "Se recomienda encarecidamente reiniciar Obsidian. Hasta que lo hagas, puede que algunos cambios no surtan efecto y que la interfaz se muestre de forma inconsistente. ¿Seguro que quieres reiniciar ahora?", + "Ui.Settings.Patches.RemediationRestartMessage": "Se recomienda encarecidamente reiniciar Obsidian. Hasta que lo hagas puede que algunos cambios no surtan efecto y que la interfaz se muestre de forma inconsistente. ¿Seguro que quieres reiniciar ahora?", "Ui.Settings.Patches.RemediationRestartNow": "Reiniciar ahora", "Ui.Settings.Patches.RemediationSuffixChanged": "El sufijo ha cambiado. Reabriendo la base de datos...", "Ui.Settings.Patches.RemediationWithValue": "Límite: ${date} (${timestamp})", @@ -1331,7 +1331,7 @@ "Ui.Settings.Remote.DisplayName": "Nombre visible", "Ui.Settings.Remote.DuplicateRemote": "Duplicar el remoto", "Ui.Settings.Remote.DuplicateRemoteSuffix": "${name} (copia)", - "Ui.Settings.Remote.E2EEConfiguration": "Configuración del E2EE", + "Ui.Settings.Remote.E2EEConfiguration": "Configuración de E2EE", "Ui.Settings.Remote.Export": "Exportar", "Ui.Settings.Remote.FetchRemoteSettings": "Obtener los ajustes remotos", "Ui.Settings.Remote.ImportConnection": "Importar conexión", @@ -1392,7 +1392,7 @@ "Ui.SetupWizard.OutroAskUserMode.Question": "Selecciona tu situación.", "Ui.SetupWizard.OutroAskUserMode.Title": "Casi terminado: se requiere una decisión", "Ui.SetupWizard.OutroNewP2PUser.GuidanceNotice": "En P2P no hay una copia en un servidor central que sobrescribir. Este paso prepara solo este dispositivo; mantenlo conectado cuando otro dispositivo obtenga sus datos iniciales.", - "Ui.SetupWizard.OutroNewP2PUser.GuidancePrimary": "La conexión punto a punto se ha configurado correctamente. A continuación, la base de datos local de LiveSync se construirá a partir de los archivos actuales de este Vault.", + "Ui.SetupWizard.OutroNewP2PUser.GuidancePrimary": "La conexión punto a punto se ha configurado correctamente. A continuación, la base de datos local de LiveSync se construirá a partir de los archivos actuales de esta bóveda.", "Ui.SetupWizard.OutroNewP2PUser.Important": "TEN EN CUENTA", "Ui.SetupWizard.OutroNewP2PUser.Proceed": "Reiniciar y preparar este dispositivo", "Ui.SetupWizard.OutroNewP2PUser.Question": "Pulsa el botón de abajo para reiniciar y pasar a la confirmación de la inicialización local.", @@ -1404,10 +1404,10 @@ "Ui.SetupWizard.OutroNewUser.Question": "Pulsa el botón de abajo para reiniciar y pasar a la confirmación final.", "Ui.SetupWizard.OutroNewUser.Title": "Configuración completada: preparando la inicialización del servidor", "Ui.SetupWizard.RebuildEverythingP2P.ConfirmLocalReset": "Entiendo que esto restablece únicamente la base de datos de sincronización local de este dispositivo.", - "Ui.SetupWizard.RebuildEverythingP2P.ConfirmLocalResetNote": "Se usarán los archivos que hay ahora en este Vault para reconstruirla.", + "Ui.SetupWizard.RebuildEverythingP2P.ConfirmLocalResetNote": "Se usarán los archivos que hay ahora en esta bóveda para reconstruirla.", "Ui.SetupWizard.RebuildEverythingP2P.ConfirmTitle": "⚠️ Confirma lo siguiente", - "Ui.SetupWizard.RebuildEverythingP2P.Guidance": "Este procedimiento descartará la base de datos local de LiveSync de este dispositivo y la reconstruirá a partir de los archivos actuales de este Vault. No elimina ni sobrescribe datos de otro dispositivo.", - "Ui.SetupWizard.RebuildEverythingP2P.Note": "Mantén este dispositivo conectado después de la inicialización para que otro dispositivo pueda obtener el Vault desde él.", + "Ui.SetupWizard.RebuildEverythingP2P.Guidance": "Este procedimiento descartará la base de datos local de LiveSync de este dispositivo y la reconstruirá a partir de los archivos actuales de esta bóveda. No elimina ni sobrescribe datos de otro dispositivo.", + "Ui.SetupWizard.RebuildEverythingP2P.Note": "Mantén este dispositivo conectado después de la inicialización para que otro dispositivo pueda obtener la bóveda desde él.", "Ui.SetupWizard.RebuildEverythingP2P.Proceed": "Lo entiendo, preparar este dispositivo", "Ui.SetupWizard.RebuildEverythingP2P.Title": "Confirmación final: preparar este dispositivo para P2P", "Ui.SetupWizard.SelectExisting.Guidance": "Estás añadiendo este dispositivo a una configuración de sincronización existente.", @@ -1433,7 +1433,7 @@ "Ui.SetupWizard.SelectNew.Title": "Método de conexión", "Ui.SetupWizard.SetupRemote.BucketOption": "Almacenamiento de objetos compatible con S3", "Ui.SetupWizard.SetupRemote.BucketOptionDesc": "Sincronización mediante archivos de diario. Necesitas tener ya un servicio de almacenamiento de objetos compatible con S3, como Amazon S3, MinIO o Cloudflare R2.", - "Ui.SetupWizard.SetupRemote.CouchDbOptionDesc": "Es el método de sincronización más adecuado para el diseño actual y ofrece todas las funciones. Necesitas tener ya una instancia de CouchDB en marcha.", + "Ui.SetupWizard.SetupRemote.CouchDbOptionDesc": "Es el método de sincronización más adecuado para el diseño actual y ofrece todas las funciones. Necesitas tener una instancia de CouchDB en marcha.", "Ui.SetupWizard.SetupRemote.Guidance": "Selecciona el tipo de remoto para esta configuración de sincronización.", "Ui.SetupWizard.SetupRemote.P2POption": "Punto a punto (P2P)", "Ui.SetupWizard.SetupRemote.P2POptionDesc": "Permite la sincronización directa entre dispositivos. No hace falta servidor, pero ambos dispositivos deben estar conectados a la vez y algunas funciones pueden estar limitadas. Solo se necesita internet para la señalización, no para transferir los datos.", @@ -1446,7 +1446,7 @@ "Updating list...": "Actualizando lista...", "URL": "URL", "Use a custom passphrase": "Usar una frase de contraseña personalizada", - "Use a Setup URI (Recommended)": "Usar un URI de configuración (recomendado)", + "Use a Setup URI (Recommended)": "Usar URI de configuración (recomendado)", "Use Custom HTTP Handler": "Usar manejador HTTP personalizado", "Use Diagnostic RTCPeerConnection for statistics": "Usar el RTCPeerConnection de diagnóstico para las estadísticas", "Use dynamic iteration count": "Usar conteo de iteraciones dinámico", @@ -1471,8 +1471,8 @@ "We can use only Secure (HTTPS) connections on Obsidian Mobile.": "En Obsidian Mobile solo se pueden usar conexiones seguras (HTTPS).", "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": "No podemos cambiar el nombre del dispositivo mientras esta función esté habilitada. Deshabilita la función para cambiarlo.", "We have to configure the device name": "Hay que configurar el nombre del dispositivo", - "We recommend that you copy your Vault folder to a safe location. This will provide a safeguard in case a large number of conflicts arise, or if you accidentally synchronise with an incorrect destination.": "Te recomendamos copiar la carpeta de tu Vault a un lugar seguro. Así tendrás una salvaguarda en caso de que surjan muchos conflictos o de que sincronices por error con un destino incorrecto.", - "We will now guide you through a few questions to simplify the synchronisation setup.": "Ahora le guiaremos con unas pocas preguntas para simplificar la configuración de la sincronización。", + "We recommend that you copy your Vault folder to a safe location. This will provide a safeguard in case a large number of conflicts arise, or if you accidentally synchronise with an incorrect destination.": "Te recomendamos copiar la carpeta de tu bóveda a un lugar seguro. Así tendrás una copia de seguridad en caso de que surjan muchos conflictos o de que sincronices por error con un destino incorrecto.", + "We will now guide you through a few questions to simplify the synchronisation setup.": "Ahora te guiaremos con unas pocas preguntas para simplificar la configuración de la sincronización。", "We will now proceed with the server configuration.": "Ahora continuaremos con la configuración del servidor。", "Welcome to Self-hosted LiveSync": "Bienvenido a Self-hosted LiveSync", "When you save a file in the editor, start a sync automatically": "Iniciar sincronización automática al guardar en editor", @@ -1482,9 +1482,9 @@ "xxhash64 (Fastest)": "xxhash64 (el más rápido)", "Yes, I want to add this device to my existing synchronisation": "Sí, quiero añadir este dispositivo a mi sincronización existente", "Yes, I want to set up a new synchronisation": "Sí, quiero configurar una nueva sincronización", - "You are adding this device to an existing synchronisation setup.": "Está añadiendo este dispositivo a una configuración de sincronización existente。", + "You are adding this device to an existing synchronisation setup.": "Estás añadiendo este dispositivo a una configuración de sincronización existente。", "You can configure in the Obsidian Plugin Settings.": "Puedes configurarlo en los ajustes del complemento de Obsidian.", "You should create a new synchronisation destination and rebuild your data there.": "Deberías crear un nuevo destino de sincronización y reconstruir allí tus datos.", - "You should perform this operation only in exceptional circumstances, such as when the server data is completely corrupted, when changes on all other devices are no longer needed, or when the database size has become unusually large in comparison to the Vault size.": "Solo deberías realizar esta operación en circunstancias excepcionales: cuando los datos del servidor estén completamente corruptos, cuando ya no necesites los cambios de los demás dispositivos o cuando el tamaño de la base de datos sea inusualmente grande respecto al del Vault.", + "You should perform this operation only in exceptional circumstances, such as when the server data is completely corrupted, when changes on all other devices are no longer needed, or when the database size has become unusually large in comparison to the Vault size.": "Solo deberías realizar esta operación en circunstancias excepcionales: cuando los datos del servidor estén completamente corruptos, cuando ya no necesites los cambios de los demás dispositivos o cuando el tamaño de la base de datos sea inusualmente grande respecto a la bóveda.", "you wanted(Thank you)!": "tu solicitud (¡gracias!)" } From 53776d244c660971b30a7c87071184484aa5461d Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sat, 26 Sep 2026 17:13:29 +0000 Subject: [PATCH 06/58] Record published 1.0.31 pre-release metadata --- docs/releases/1.0.md | 58 ++++++++++++++++++++++++++++++++++++++ updates.md | 66 ++++---------------------------------------- versions.json | 3 +- 3 files changed, 66 insertions(+), 61 deletions(-) diff --git a/docs/releases/1.0.md b/docs/releases/1.0.md index 941ac6ff..46584f8b 100644 --- a/docs/releases/1.0.md +++ b/docs/releases/1.0.md @@ -2,6 +2,64 @@ This document contains earlier published releases from the 1.0 line of the [current Self-hosted LiveSync release history](../../updates.md). Beta and release-candidate builds published before 1.0.0 are recorded in the [1.0 preview history](1.0-previews.md). Earlier release lines continue in the [0.25 history](0.25.md) and the [legacy history](legacy.md). +## 1.0.26 + +~~1.0.25~~ was cancelled because pre-release validation found that LiveSync could appear to finish synchronising even though Android had not written a received file to the Vault; the warning appeared only after restart. + +6th September, 2026 + +### Synchronisation and storage + +#### Fixed + +- Files inside a folder are no longer silently removed from synchronisation when an external tool changes only the letter case of that folder while Obsidian is running. This prevents the stale deletion from reaching other devices or later removing the local file. Moving files into ignored or otherwise excluded locations retains the existing behaviour, and the folder-name case itself may still differ between devices. (#1168) +- A problem processing one file during ordinary start-up no longer prevents every other file from synchronising. LiveSync warns about the affected files and can retry them later; Fetch and Rebuild still stop if they cannot finish safely. (#1164) +- When LiveSync cannot finish preparing this device for synchronisation, it now says that synchronisation is unavailable and directs you to generate a report, instead of remaining at 'Not ready'. (#1164) + +#### Improved + +- When LiveSync cannot write a received file to the Vault, it now warns immediately instead of appearing to have synchronised it successfully. The generated report identifies the affected path, and a later scan can try it again. + +### Conflict handling and recovery + +#### Improved + +- Conflict resolution dialogues now close when the same file is resolved elsewhere or when the plug-in unloads. Requests for different files are shown one at a time, while a newer request for the same file replaces the older one. + +### Setup and compatibility + +#### Improved + +- Unconfigured Vaults now stay focused on setup instead of running Config Doctor or incomplete-document checks before they can be used. Returning a configured Vault to an unconfigured state also stops those checks until the requested restart. (#1161) +- When the active file contains a file or folder name longer than 255 UTF-8 bytes, LiveSync now explains that the path may not work on some Android and Linux file systems. It does not rename or reject the file. (#1164) + +## 1.0.24 + +3rd September, 2026 + +### Interface and translation + +#### Fixed + +- The Setup Wizard now correctly explains that the existing-device path adds this device to an existing synchronisation (PR #1118). Thank you to @nikhilmaddirala for the contribution! +- Spanish translations now resolve the **Display language** placeholder, cover previously untranslated Setup Wizard and CouchDB text, translate user-facing Config Doctor values and confirmation controls, and use Spanish sentence case (PR #1129). Thank you to @zeedif for the contribution! + +#### Improved + +- The Setup Wizard now shows the passphrase and **Obfuscate Properties** controls only after E2EE is enabled, provides a password-visibility button, allows longer translated labels to wrap, and keeps the invitation link compact on desktop while preserving its mobile touch target (PR #1130). Thank you to @zeedif for the contribution! + +### Synchronisation and storage + +#### Fixed + +- **Overwrite Server Data with This Device's Files** now keeps this device's synchronisation settings instead of reapplying settings from the remote database which is about to be replaced. Enabling E2EE before a rebuild therefore remains enabled and uploads encrypted data. (#1146) + +### Command-line tool + +#### Fixed + +- The systemd installer now finds the repository root correctly, installs every generated bundle chunk and required production dependency, checks the installed command before activation, and reports success only when the service remains active. + ## 1.0.23 2nd September, 2026 diff --git a/updates.md b/updates.md index c1fdf0e0..f1cb1f32 100644 --- a/updates.md +++ b/updates.md @@ -12,14 +12,18 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi ## Unreleased +## 1.0.31 + +26th September, 2026 + ### Synchronisation and storage #### Fixed - Files with colons in their names now retain their full paths in synchronisation data instead of appearing as incorrectly named copies at the Vault root. (#1206) - Obsidian may refuse to create a missing file with such a name. LiveSync also treats these names as invalid on Windows and Android, so the file may not appear in those devices' Vaults. Existing misplaced copies are left for you to review; this change does not remove them automatically. -- Received changes are applied again while remediation mode is active. That mode prevents the scan which readiness depends upon, so nothing had been applied since the plug-in began waiting for readiness, not even changes older than the configured modification-time limit. The limit itself is still enforced for every change, and application waits for a usable local database so that a change arriving during a fetch is not dropped. -- A scheduled fetch no longer offers Simple Fetch while remediation mode is active. Simple Fetch reconciles the Vault with the local database past the restriction, which could store the current files or apply changes newer than the limit; the detailed flow states the restriction and offers to clear it first (#1202). Thank you to @kimjansheden for both fixes and the regression tests in PR #1208! +- Received changes within the configured modification-time limit are applied to the Vault again while remediation mode is active. Changes newer than the limit remain blocked; changes arriving while a fetch makes the local database unavailable are kept for a later attempt. +- A scheduled fetch no longer offers Simple Fetch while remediation mode is active. This prevents the fetch from bypassing the modification-time limit; the detailed flow explains the restriction and offers to clear it first (#1202). Thank you to @kimjansheden for both fixes and the regression tests in PR #1208! - On start-up, an unchanged file with a missing local revision record can be recognised before newer content arrives, avoiding an unnecessary conflict. Files with actual local edits still require conflict review. (#1207) ## 1.0.30 @@ -87,61 +91,3 @@ For now, I am addressing the issues I can resolve first. I hope this helps. #### Fixed - First-time Object Storage setup now completes when **Use Custom HTTP Handler** is enabled for an empty remote, including a new Cloudflare R2 bucket. LiveSync can now create the remote state required to begin synchronisation. (#1166) - -## 1.0.26 - -~~1.0.25~~ was cancelled because pre-release validation found that LiveSync could appear to finish synchronising even though Android had not written a received file to the Vault; the warning appeared only after restart. - -6th September, 2026 - -### Synchronisation and storage - -#### Fixed - -- Files inside a folder are no longer silently removed from synchronisation when an external tool changes only the letter case of that folder while Obsidian is running. This prevents the stale deletion from reaching other devices or later removing the local file. Moving files into ignored or otherwise excluded locations retains the existing behaviour, and the folder-name case itself may still differ between devices. (#1168) -- A problem processing one file during ordinary start-up no longer prevents every other file from synchronising. LiveSync warns about the affected files and can retry them later; Fetch and Rebuild still stop if they cannot finish safely. (#1164) -- When LiveSync cannot finish preparing this device for synchronisation, it now says that synchronisation is unavailable and directs you to generate a report, instead of remaining at 'Not ready'. (#1164) - -#### Improved - -- When LiveSync cannot write a received file to the Vault, it now warns immediately instead of appearing to have synchronised it successfully. The generated report identifies the affected path, and a later scan can try it again. - -### Conflict handling and recovery - -#### Improved - -- Conflict resolution dialogues now close when the same file is resolved elsewhere or when the plug-in unloads. Requests for different files are shown one at a time, while a newer request for the same file replaces the older one. - -### Setup and compatibility - -#### Improved - -- Unconfigured Vaults now stay focused on setup instead of running Config Doctor or incomplete-document checks before they can be used. Returning a configured Vault to an unconfigured state also stops those checks until the requested restart. (#1161) -- When the active file contains a file or folder name longer than 255 UTF-8 bytes, LiveSync now explains that the path may not work on some Android and Linux file systems. It does not rename or reject the file. (#1164) - -## 1.0.24 - -3rd September, 2026 - -### Interface and translation - -#### Fixed - -- The Setup Wizard now correctly explains that the existing-device path adds this device to an existing synchronisation (PR #1118). Thank you to @nikhilmaddirala for the contribution! -- Spanish translations now resolve the **Display language** placeholder, cover previously untranslated Setup Wizard and CouchDB text, translate user-facing Config Doctor values and confirmation controls, and use Spanish sentence case (PR #1129). Thank you to @zeedif for the contribution! - -#### Improved - -- The Setup Wizard now shows the passphrase and **Obfuscate Properties** controls only after E2EE is enabled, provides a password-visibility button, allows longer translated labels to wrap, and keeps the invitation link compact on desktop while preserving its mobile touch target (PR #1130). Thank you to @zeedif for the contribution! - -### Synchronisation and storage - -#### Fixed - -- **Overwrite Server Data with This Device's Files** now keeps this device's synchronisation settings instead of reapplying settings from the remote database which is about to be replaced. Enabling E2EE before a rebuild therefore remains enabled and uploads encrypted data. (#1146) - -### Command-line tool - -#### Fixed - -- The systemd installer now finds the repository root correctly, installs every generated bundle chunk and required production dependency, checks the installed command before activation, and reports success only when the service remains active. diff --git a/versions.json b/versions.json index 5332e341..1489c851 100644 --- a/versions.json +++ b/versions.json @@ -41,5 +41,6 @@ "1.0.27": "1.7.2", "1.0.28": "1.7.2", "1.0.29": "1.7.2", - "1.0.30": "1.7.2" + "1.0.30": "1.7.2", + "1.0.31": "1.7.2" } From b88bebcaaffa72dbbc0684d95c412c627bb03403 Mon Sep 17 00:00:00 2001 From: Zeedif Date: Sat, 26 Sep 2026 11:58:27 -0600 Subject: [PATCH 07/58] refactor(i18n): replace ambiguous Maintenance lock-dialog keys with source text Co-Authored-By: Claude Sonnet 5 --- src/common/messages/combinedMessages.prod.ts | 62 ++++++++++---------- src/common/messagesJson/en.json | 8 +-- src/common/messagesJson/es.json | 8 +-- src/common/messagesJson/ko.json | 8 +-- src/common/messagesJson/zh-tw.json | 8 +-- src/common/messagesJson/zh.json | 8 +-- src/common/messagesYAML/en.yaml | 8 +-- src/common/messagesYAML/es.yaml | 18 ++---- src/common/messagesYAML/ko.yaml | 8 +-- src/common/messagesYAML/zh-tw.yaml | 8 +-- src/common/messagesYAML/zh.yaml | 8 +-- 11 files changed, 72 insertions(+), 80 deletions(-) diff --git a/src/common/messages/combinedMessages.prod.ts b/src/common/messages/combinedMessages.prod.ts index 0bcbaaf2..f44cf9c9 100644 --- a/src/common/messages/combinedMessages.prod.ts +++ b/src/common/messages/combinedMessages.prod.ts @@ -2592,6 +2592,13 @@ export const allMessages: Readonly Date: Sun, 27 Sep 2026 01:56:26 +0000 Subject: [PATCH 08/58] Restore encrypted path reception with Commonlib 0.1.29 --- package-lock.json | 8 ++++---- package.json | 2 +- updates.md | 6 ++++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 01b9f1d2..dbf10b07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", - "@vrtmrz/livesync-commonlib": "0.1.28", + "@vrtmrz/livesync-commonlib": "0.1.29", "@vrtmrz/obsidian-plugin-kit": "0.1.4", "@vrtmrz/ui-interactions": "0.1.2", "diff-match-patch": "^1.0.5", @@ -4567,9 +4567,9 @@ } }, "node_modules/@vrtmrz/livesync-commonlib": { - "version": "0.1.28", - "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.28.tgz", - "integrity": "sha512-3NXswTtOU+fE4KRxBWHfkMx9r2Eu9IFH9N/ibj75C5eLh1tFPH7n87bGib6La7Twz9yCDIJRBsBUc2tx81rxOQ==", + "version": "0.1.29", + "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.29.tgz", + "integrity": "sha512-PeBUUQNSuyS+ISPIvwK3rzaritHi0XDfNlFMOqaRpIbxGAWbWK0YDtE1Htbde3r8Lr3ZdTNjpY+Nmr//OxzgTA==", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.808.0", diff --git a/package.json b/package.json index f2e8dc1b..aa67ed34 100644 --- a/package.json +++ b/package.json @@ -183,7 +183,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", - "@vrtmrz/livesync-commonlib": "0.1.28", + "@vrtmrz/livesync-commonlib": "0.1.29", "@vrtmrz/obsidian-plugin-kit": "0.1.4", "@vrtmrz/ui-interactions": "0.1.2", "diff-match-patch": "^1.0.5", diff --git a/updates.md b/updates.md index f1cb1f32..eb361f65 100644 --- a/updates.md +++ b/updates.md @@ -12,6 +12,12 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi ## Unreleased +### Synchronisation and storage + +#### Fixed + +- The receiving device now accepts encrypted file information when both end-to-end encryption and Path Obfuscation are enabled. The 1.0.31 pre-release could reject this information, leaving files from another device absent from the Vault. + ## 1.0.31 26th September, 2026 From 1ce3caf2ad05f876525a485216aadd09ccb7e1af Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 02:14:24 +0000 Subject: [PATCH 09/58] Releasing 1.0.32 --- manifest.json | 2 +- package-lock.json | 10 +++++----- package.json | 2 +- src/apps/cli/package.json | 2 +- src/apps/webapp/package.json | 2 +- src/apps/webpeer/package.json | 2 +- updates.md | 4 ++++ versions.json | 3 ++- 8 files changed, 16 insertions(+), 11 deletions(-) diff --git a/manifest.json b/manifest.json index 6d9bcb18..93b3ff90 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "1.0.30", + "version": "1.0.32", "minAppVersion": "1.7.2", "description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "author": "vorotamoroz", diff --git a/package-lock.json b/package-lock.json index dbf10b07..78086c95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "1.0.30", + "version": "1.0.32", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "1.0.30", + "version": "1.0.32", "license": "MIT", "workspaces": [ "src/apps/cli", @@ -12665,7 +12665,7 @@ }, "src/apps/cli": { "name": "self-hosted-livesync-cli", - "version": "1.0.30-cli", + "version": "1.0.32-cli", "dependencies": { "chokidar": "^4.0.0", "minimatch": "^10.2.5", @@ -12690,7 +12690,7 @@ }, "src/apps/webapp": { "name": "livesync-webapp", - "version": "1.0.30-webapp", + "version": "1.0.32-webapp", "dependencies": { "octagonal-wheels": "^0.1.54" }, @@ -12702,7 +12702,7 @@ } }, "src/apps/webpeer": { - "version": "1.0.30-webpeer", + "version": "1.0.32-webpeer", "dependencies": { "octagonal-wheels": "^0.1.54" }, diff --git a/package.json b/package.json index aa67ed34..d87efd90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "1.0.30", + "version": "1.0.32", "description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "main": "main.js", "type": "module", diff --git a/src/apps/cli/package.json b/src/apps/cli/package.json index 0b4a51f4..f85b56a9 100644 --- a/src/apps/cli/package.json +++ b/src/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "self-hosted-livesync-cli", "private": true, - "version": "1.0.30-cli", + "version": "1.0.32-cli", "main": "dist/index.cjs", "type": "module", "scripts": { diff --git a/src/apps/webapp/package.json b/src/apps/webapp/package.json index 8e05dce8..f8764ab7 100644 --- a/src/apps/webapp/package.json +++ b/src/apps/webapp/package.json @@ -1,7 +1,7 @@ { "name": "livesync-webapp", "private": true, - "version": "1.0.30-webapp", + "version": "1.0.32-webapp", "type": "module", "description": "Browser-based Self-hosted LiveSync using FileSystem API", "scripts": { diff --git a/src/apps/webpeer/package.json b/src/apps/webpeer/package.json index d245db9d..adbda057 100644 --- a/src/apps/webpeer/package.json +++ b/src/apps/webpeer/package.json @@ -1,7 +1,7 @@ { "name": "webpeer", "private": true, - "version": "1.0.30-webpeer", + "version": "1.0.32-webpeer", "type": "module", "scripts": { "dev": "vite", diff --git a/updates.md b/updates.md index eb361f65..67ba3238 100644 --- a/updates.md +++ b/updates.md @@ -12,6 +12,10 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi ## Unreleased +## 1.0.32 + +27th September, 2026 + ### Synchronisation and storage #### Fixed diff --git a/versions.json b/versions.json index 1489c851..c56f86ab 100644 --- a/versions.json +++ b/versions.json @@ -42,5 +42,6 @@ "1.0.28": "1.7.2", "1.0.29": "1.7.2", "1.0.30": "1.7.2", - "1.0.31": "1.7.2" + "1.0.31": "1.7.2", + "1.0.32": "1.7.2" } From 7b3b6ff854f2ff10452be3eaf8e0cd76ac65ac2b Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 27 Sep 2026 02:16:21 +0000 Subject: [PATCH 10/58] Carry 1.0.31 changes into 1.0.32 release notes --- updates.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/updates.md b/updates.md index 67ba3238..247204cc 100644 --- a/updates.md +++ b/updates.md @@ -16,11 +16,18 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi 27th September, 2026 +The 1.0.31 pre-release was not promoted after validation found that a receiving device could reject encrypted CouchDB changes when Path Obfuscation was enabled. This release includes its changes and corrects that issue. + ### Synchronisation and storage #### Fixed - The receiving device now accepts encrypted file information when both end-to-end encryption and Path Obfuscation are enabled. The 1.0.31 pre-release could reject this information, leaving files from another device absent from the Vault. +- Files with colons in their names now retain their full paths in synchronisation data instead of appearing as incorrectly named copies at the Vault root. (#1206) + - Obsidian may refuse to create a missing file with such a name. LiveSync also treats these names as invalid on Windows and Android, so the file may not appear in those devices' Vaults. Existing misplaced copies are left for you to review; this change does not remove them automatically. +- Received changes within the configured modification-time limit are applied to the Vault again while remediation mode is active. Changes newer than the limit remain blocked; changes arriving while a fetch makes the local database unavailable are kept for a later attempt. +- A scheduled fetch no longer offers Simple Fetch while remediation mode is active. This prevents the fetch from bypassing the modification-time limit; the detailed flow explains the restriction and offers to clear it first (#1202). Thank you to @kimjansheden for both fixes and the regression tests in PR #1208! +- On start-up, an unchanged file with a missing local revision record can be recognised before newer content arrives, avoiding an unnecessary conflict. Files with actual local edits still require conflict review. (#1207) ## 1.0.31 From 6c50505096b13e9ded2131fd4ad1fb936003c8df Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 27 Sep 2026 09:51:53 +0000 Subject: [PATCH 11/58] Integrate encrypted internal metadata in LiveSync --- ...elease_notes_and_database_compatibility.md | 4 +- .../internal_metadata_encryption.md | 198 ++++++++++++ docs/settings.md | 8 + docs/troubleshooting.md | 6 + package.json | 1 + src/common/replicatorConfigurationIdentity.ts | 2 + ...plicatorConfigurationIdentity.unit.spec.ts | 16 + .../ObsidianLiveSyncSettingTab.ts | 1 + .../SettingDialogue/PaneRemoteConfig.ts | 34 ++- .../PaneRemoteConfig.unit.spec.ts | 58 +++- .../dialogs/SetupRemoteE2EE.svelte | 23 +- .../replication/ReplicateResultProcessor.ts | 255 ++++++++++++++-- .../ReplicateResultProcessor.unit.spec.ts | 285 +++++++++++++++++- .../centralCompatibilityRecovery.ts | 22 +- .../centralCompatibilityRecovery.unit.spec.ts | 58 +++- src/serviceFeatures/replication/index.ts | 14 +- .../replicationFeature.unit.spec.ts | 71 ++++- test/e2e-obsidian/README.md | 2 + .../scripts/cli-to-obsidian-sync.ts | 2 + .../scripts/customisation-sync.ts | 19 ++ .../scripts/hidden-file-snippet-sync.ts | 17 ++ .../scripts/remote-feature-change.ts | 178 +++++++++++ updates.md | 7 + 23 files changed, 1220 insertions(+), 61 deletions(-) create mode 100644 docs/design_docs/internal_metadata_encryption.md create mode 100644 test/e2e-obsidian/scripts/remote-feature-change.ts diff --git a/docs/adr/2026_07_release_notes_and_database_compatibility.md b/docs/adr/2026_07_release_notes_and_database_compatibility.md index 5a7d80aa..4985008d 100644 --- a/docs/adr/2026_07_release_notes_and_database_compatibility.md +++ b/docs/adr/2026_07_release_notes_and_database_compatibility.md @@ -57,6 +57,8 @@ Keep configured-state inference separate from new-Vault initialisation. If an ex - On resume, clear `versionUpFlash` and persist that fail-closed change before recording the current `VER` as acknowledged. If saving fails, restore the gate. Reapply settings only after the marker has advanced so that the previously configured synchronisation behaviour can resume without reconstruction. - Preserve the original legacy review message as a structured reason when no more specific database or settings-schema reason is available. Escape it before including it in Markdown UI. - Continue to reject a remote version document which is newer than the running implementation. That receiver-side check is independent of the local upgrade review. +- From remote generation 13, assess the `used_features` list in that document as a separate compatibility dimension. A client must recognise every listed feature before it interprets the database or runs maintenance which depends on Metadata. Declare a feature before writing its representation, and retain the declaration while older data may depend on it. An unknown identifier is reported as text without requiring a descriptive label in that client. +- Do not advance the device-local `VER` acknowledgement merely because a remote feature is introduced. The remote generation and its feature list govern remote admission; `VER` remains the local compatibility review gate. Connecting to a generation-12 database does not promote it solely because the client understands generation 13. ### Onboarding activation and initialisation @@ -90,7 +92,7 @@ Keep configured-state inference separate from new-Vault initialisation. If an ex - Accepted new-device and existing-device setup cannot enable ordinary processing before the selected Rebuild or Fetch has been reserved. - An older installation cannot dismiss evidence that a newer implementation or settings schema has already been used on the device. - The Obsidian-specific dialogue depends only on a host-neutral compatibility result and the injected confirmation capability. Commonlib remains responsible for settings migration, device-local storage, and the replication gate. -- A future incompatible database change must increment `VER`, provide an actionable review message, verify the remote version negotiation, and test both the pending and acknowledged states. A major SemVer increase without those changes has no database-compatibility effect. +- A future local database change which requires compatibility review must increment `VER`, provide an actionable review message, and test both the pending and acknowledged states. A new remote representation must declare its feature before use and verify remote admission independently. A major SemVer increase alone has no database-compatibility effect. ## Verification diff --git a/docs/design_docs/internal_metadata_encryption.md b/docs/design_docs/internal_metadata_encryption.md new file mode 100644 index 00000000..c73d0d01 --- /dev/null +++ b/docs/design_docs/internal_metadata_encryption.md @@ -0,0 +1,198 @@ +--- +date: 2026-09-27 +commonlib-version: "0.1.30" +self-hosted-livesync-version: "1.0.32" +status: unreleased +--- + +# Internal Metadata encryption and remote feature changes + +This document defines the LiveSync integration of Commonlib's remote feature +contract and encrypted Metadata for Hidden File Sync and Customisation Sync. +It describes unreleased behaviour being implemented in this branch. + +Commonlib's companion `docs/remote-feature-compatibility.md` is the +source of truth for the wire document, identifiers, validation, and shared +assessment. This document owns the application behaviour, settings, Doctor +recommendation, and verification of the Obsidian and CLI integrations. + +## Scope and settings + +Add `encryptInternalMetadata` to the shared encryption settings. A genuinely new +Vault or CLI configuration defaults to true. Existing stored settings and old +Setup URI or QR imports complete an absent value as false. Ordinary partial +setting updates retain the current value. + +The preference applies to CouchDB with E2EE V2 and Property Encryption enabled. +Show the preference as unavailable and explain its prerequisites when they are +absent. Keep Journal and P2P's existing +transport protection and avoid unrelated setting mismatches for those remotes. + +Use the existing HKDF Metadata representation to protect path, creation and +modification times, size, and Chunk references for obfuscated internal entries. +Keep the `i:`, `ix:`, and supported legacy `ps:` document IDs, path conversion, +and content Chunk representation. Read encrypted Metadata independently of the +write preference, including after that preference is disabled. + +The protection leaves document IDs, namespaces, revisions, deletion state, +document counts, and ciphertext lengths visible. It does not encrypt device or +Vault names stored in separate participant records. + +## Enabling the preference + +Changing the preference does not automatically reconstruct a database or gather +all devices' data. It affects subsequent Metadata writes. Unchanged documents +and old revisions can retain plaintext; mixed plaintext and encrypted Metadata +are a supported transition state. + +Strongly recommend the existing manual remote Rebuild workflow when the person +wants existing Metadata protected as well. The person prepares the authoritative +data for that workflow. Describe this distinction in the setting, Doctor reason, +and operational documentation. Do not advertise complete historical protection +merely because the preference is enabled. + +Copy the preference with the other encryption settings when preparing a remote +profile. Recreate a connection when its effective encryption settings change. +Use the existing Tweak assessment and manual mismatch resolution; do not change +the remote's shared policy silently when importing or loading settings. + +## Doctor + +Use Commonlib's existing conditional recommendation rules. Recommend true when +the selected CouchDB settings have E2EE V2 and Property Encryption enabled and +the new preference is false. Do not require Hidden File Sync or Customisation +Sync to be active before offering the recommendation. + +Retain the existing E2EE V2 recommendation for a legacy algorithm. After that +change, ensure the newly applicable Metadata recommendation is not hidden by a +premature `doctorProcessedVersion` update. Advance the Doctor rule revision so +an older completed consultation does not suppress this new recommendation. + +Apply the preference only when the person accepts the recommendation. Include +the existing-data limitation, the manual Rebuild recommendation, and the need +for compatible clients in the explanation. Do not set `requireRebuild` or +`requireRebuildLocal` for this rule: the current host wrapper can schedule those +operations and restart. `recommendRebuild` currently exists only as an unused +rule field, so setting it alone does not display an explanation. + +## Receiving a changed version document + +The existing path is: + +1. Commonlib receives a CouchDB replication change and calls + `parseSynchroniseResult` with the received documents. +2. The replication service feature passes them to + `ReplicateResultProcessor.enqueueAll`. +3. `processIfNonDocumentChange` recognises `type: versioninfo` and requests active + Replicator retirement when `version > VER`. +4. The owner closes admission, requests transfer cancellation, drains its work, + and closes the instance. The result callback does not wait for that transition. + +Retain this observation path, but use Commonlib's complete assessment of the +identified control document. A changed `used_features` list must be inspected +even when the numeric version is unchanged. A mere revision change, list reorder, +or duplicate known identifier does not require an interruption. + +Inspect the entire batch's control information before passing any file entries +to normal or optional processing. Recognise the fixed control-document ID and +validate its type and contents. A deleted or malformed control document is a +rejection, not a successful empty update. + +When all requirements remain supported, refresh the assessment and affected +shared-setting checks; a newly added feature retires the current writer so its +next admission rechecks the shared Tweak policy. When a requirement is +unknown or incompatible, synchronously record the block for the affected +database, stop admitting new reflection and database operations, and request +retirement through the existing owner. Notify with the unknown identifiers as +text, using a generic message when no descriptive label exists. + +File application and remote transfer have separate lifetimes. Requesting owner +retirement alone is not the application block. Keep the block separate from +temporary lifecycle suspension so an ordinary resume event cannot clear it. +Queued or waiting work checks it before starting another write; notifications +from an old physical database must not affect its replacement. + +Do not await `onCloseActiveReplication` inside the callback which delivered the +change. That callback can belong to work which retirement must drain. Establish +the block immediately, request retirement without awaiting it there, and let the +owner perform cancellation and close in its existing order. + +## Persistence and recovery boundaries + +A CouchDB replication notification can arrive after the documents have entered +the local DB. Already-started network and filesystem operations may settle. +This feature does not promise rollback or atomic revocation of those operations. + +Preserve pending work or durable reconciliation information when stopping. A +checkpoint may already include the documents which have not reached the Vault. +Do not drop those documents or depend on an ordinary reconnect to send them +again. A compatible client must reassess and reprocess or explicitly reacquire +them before lifting the applicable block. +Persist a blocked pending-work snapshot before the received-change callback +settles, while requesting owner retirement separately to avoid a circular wait. + +Restore compatibility checks before replication result application and the next +ordinary synchronisation. Retain observed feature requirements with the pending +work snapshot so that a shortened version-document list does not release a +blocked local database on restart. A dismissed Notice or a changed connection +does not establish that the affected local data has become interpretable. +An older local generation without feature declarations remains readable during +normal application and cleaned-remote recovery; remote migration remains the +responsibility of the replication admission check. + +Garbage Collection V3 is a beta manual operation which begins with an ordinary +bidirectional synchronisation. That admission checks the remote feature +contract; no additional per-step GC checks are introduced. The separate +cleaned-remote recovery path checks the local version document before its +first Chunk-reference count because it does not start with that synchronisation. + +Use the same Commonlib assessment at the CLI, Fast Fetch, and direct-access +boundaries. The Obsidian result processor is one consumer, not the only place +which determines compatibility. Keep unrelated Vaults and databases operational. + +## Verification and documentation + +Keep focused tests for settings defaults and imports, the Doctor condition +matrix, acceptance and dismissal, connection replacement, and absence of an +automatic Rebuild, Fetch, or restart for this rule. + +Add deterministic runtime tests for feature-only changes, version documents +first and last in a batch, unknown-name presentation, duplicate notifications, +queued and waiting reflection, stale database callbacks, restart, checkpointed +but unapplied documents, and cancellation without a circular wait. + +Before this implementation, the host processor was checked with a focused unit probe: a numeric +incompatibility requests retirement, but the processor still applies a note in +the same batch when its host remains ready. A same-version document with an +unknown feature does not request retirement. These observations motivated the +new checks. + +Extend real Obsidian Hidden File Sync and Customisation Sync scenarios and the +CLI interoperability checks. Inspect raw CouchDB documents as well as restored +files. Exercise an active connection when another client changes the feature +requirements, and verify that previously accepted data survives the stop. +Pending-work restoration and recovery with a compatible client are separate +boundaries. + +The local packed Commonlib 0.1.30 candidate passed Commonlib unit and boundary +tests and the LiveSync build, type checks, and unit tests. Real Obsidian 1.12.7 +passed the Hidden File Sync, Customisation Sync, and encrypted CLI-to-Obsidian +scenarios. The dedicated active-connection scenario changed a generation 12 +remote to generation 13 with an unknown feature whilst continuous replication +was running. The local control document arrived, the active Replicator retired, +a subsequent replication was refused, and an earlier accepted note stayed in +the Vault. Focused host tests cover both batch orders, pending-work snapshots, +restart, stale callbacks, and the older local-generation case. Recovery after +upgrading to a future client that understands the unknown feature has not been +exercised in real Obsidian. + +Keep the primary-language settings and troubleshooting guides, the +database-compatibility ADR, and Unreleased notes aligned with this behaviour. +Keep the detailed shared protocol in Commonlib and link to it after publication; +do not maintain another copy of its wire schema here. Translations are a separate +change. Update tested-version evidence when the implementation and its +validation have been accepted. + +Related application contracts: [Replicator architecture](replicator_architecture.md), +[Tweak compatibility](tweak_compatibility.md), and +[database compatibility](../adr/2026_07_release_notes_and_database_compatibility.md). diff --git a/docs/settings.md b/docs/settings.md index 2ce7e275..d6622d91 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -245,6 +245,14 @@ Setting key: usePathObfuscation In default, the path of the file is not obfuscated to improve the performance. If you enable this, the path of the file will be obfuscated. This is useful when you want to hide the path of the file. +#### Encrypt internal file Metadata + +Setting key: encryptInternalMetadata + +For CouchDB, this encrypts paths, times, sizes, and Chunk references in the Metadata used by Hidden File Sync and Customisation Sync. It requires E2EE V2 and **Property Encryption**. New Vaults enable the preference by default, but it has no effect until those prerequisites are enabled. Existing Vaults and older Setup URIs and QR codes keep it disabled unless you enable it. + +Enabling the preference protects future Metadata writes. Existing Metadata and earlier revisions can remain readable in the remote database. If you want to protect existing Metadata too, prepare the authoritative data, update every device to a compatible version, and manually Rebuild the remote database. LiveSync does not gather data or start a Rebuild when you change this preference. Plaintext and encrypted Metadata can coexist during the transition. Document IDs, revision information, document counts, and ciphertext lengths remain visible. + #### Encryption Algorithm Setting key: E2EEAlgorithm diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index d8f128b5..b2a00a68 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -96,6 +96,8 @@ Current releases automatically align compatible settings which control how new c A missing legacy file-name case setting means case-insensitive handling. It matches an explicit disabled setting and does not require a rebuild for that difference. An explicitly enabled setting can use different document IDs and still requires a compatibility decision against either value. Other configuration differences shown in the dialogue must still be resolved. +If the mismatch names **Encrypt internal file Metadata**, update every device before accepting that preference. It affects subsequent Metadata writes for Hidden File Sync and Customisation Sync; it does not automatically protect existing Metadata. A manual remote Rebuild is strongly recommended if you need to protect existing paths, times, sizes, and Chunk references. + The `Sync now` command keeps routine replication progress quiet so that it is convenient to assign to a keyboard shortcut; assign one in Obsidian if that suits your workflow. A quiet command may still open this dialogue when a mismatch or another decision requires your attention. The available actions depend on when the mismatch is found: @@ -109,6 +111,10 @@ The available actions depend on when the mismatch is found: Historic defect notices and renamed controls are retained in the [0.25 release history](releases/0.25.md) and [legacy release history](releases/legacy.md), rather than in the current troubleshooting path. +## The remote database uses an unknown feature + +When a notice identifies an unknown feature, update this device and every other client of the same CouchDB database, including the CLI. The notice includes the feature identifier even if this version has no descriptive name for it. Synchronisation and pending file reflection pause because an older client may not interpret the Metadata and its Chunk references correctly. The cleaned-remote recovery path also checks compatibility before counting Chunk references. Do not remove the feature name from the remote version document to bypass the check. After updating, reconnect and review any pending file changes before running Garbage Collection. + ## Setup and settings questions ### Share a configuration with another device diff --git a/package.json b/package.json index d87efd90..d270849e 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "test:e2e:obsidian:security-seed-reconnect": "tsx test/e2e-obsidian/scripts/security-seed-reconnect.ts", "test:e2e:obsidian:hidden-file-snippet-sync": "tsx test/e2e-obsidian/scripts/hidden-file-snippet-sync.ts", "test:e2e:obsidian:customisation-sync": "tsx test/e2e-obsidian/scripts/customisation-sync.ts", + "test:e2e:obsidian:remote-feature-change": "tsx test/e2e-obsidian/scripts/remote-feature-change.ts", "test:e2e:obsidian:setting-markdown-export": "tsx test/e2e-obsidian/scripts/setting-markdown-export.ts", "test:e2e:obsidian:upgrade-from-stable": "tsx test/e2e-obsidian/scripts/upgrade-from-stable.ts", "test:e2e:obsidian:local-suite": "tsx test/e2e-obsidian/scripts/local-suite.ts", diff --git a/src/common/replicatorConfigurationIdentity.ts b/src/common/replicatorConfigurationIdentity.ts index ff6221d8..af72f6d6 100644 --- a/src/common/replicatorConfigurationIdentity.ts +++ b/src/common/replicatorConfigurationIdentity.ts @@ -1,4 +1,5 @@ import type { RemoteDBSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { usesEncryptedInternalMetadata } from "@vrtmrz/livesync-commonlib/replication"; type EndpointProjection = readonly [kind: "url" | "invalid-url", value: string]; @@ -77,6 +78,7 @@ export function getCouchDBReplicatorConfigurationIdentity(settings: RemoteDBSett settings.useRequestAPI, settings.disableRequestURI, projectRemoteSecurity(settings), + usesEncryptedInternalMetadata(settings), settings.enableCompression, ]); } diff --git a/src/common/replicatorConfigurationIdentity.unit.spec.ts b/src/common/replicatorConfigurationIdentity.unit.spec.ts index 719db130..a5ef0f9b 100644 --- a/src/common/replicatorConfigurationIdentity.unit.spec.ts +++ b/src/common/replicatorConfigurationIdentity.unit.spec.ts @@ -67,6 +67,22 @@ describe("active Replicator configuration identity", () => { ); }); + it("recreates the CouchDB connection when internal Metadata encryption becomes effective", () => { + const active = configuredSettings({ usePathObfuscation: true, encryptInternalMetadata: false }); + const enabled = { ...active, encryptInternalMetadata: true }; + + expect(getCouchDBReplicatorConfigurationIdentity(enabled)).not.toBe( + getCouchDBReplicatorConfigurationIdentity(active) + ); + const inactive = { ...active, usePathObfuscation: false }; + expect(getCouchDBReplicatorConfigurationIdentity({ ...inactive, encryptInternalMetadata: true })).toBe( + getCouchDBReplicatorConfigurationIdentity(inactive) + ); + expect(getObjectStorageReplicatorConfigurationIdentity(enabled)).toBe( + getObjectStorageReplicatorConfigurationIdentity(active) + ); + }); + it("projects only the active CouchDB authentication mode", () => { const basic = configuredSettings({ useJWT: false, jwtKey: "inactive-a" }); expect(getCouchDBReplicatorConfigurationIdentity({ ...basic, jwtKey: "inactive-b" })).toBe( diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts index 032caf76..5bf463d1 100644 --- a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts @@ -569,6 +569,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { } } + // Internal Metadata encryption affects future Metadata writes and is not a rebuild requirement. isNeedRebuildLocal() { return this.isSomeDirty([ "useIndexedDBAdapter", diff --git a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts index 260ec3c0..3f93b00c 100644 --- a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts +++ b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts @@ -5,6 +5,7 @@ import { DEFAULT_SETTINGS, LOG_LEVEL_NOTICE, type ObsidianLiveSyncSettings, + type EncryptionSettings, LOG_LEVEL_VERBOSE, } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { Menu, type ButtonComponent } from "@/deps.ts"; @@ -31,11 +32,13 @@ import { import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString"; import type { RemoteConfigurationResult } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString"; import SetupRemote from "@/modules/features/SetupWizard/dialogs/SetupRemote.svelte"; +import SetupRemoteE2EE from "@/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte"; import SetupRemoteCouchDB from "@/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte"; import SetupRemoteBucket from "@/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte"; import type { SetupRemoteCouchDBInitialData, SetupRemoteCouchDBResultType, + SetupRemoteE2EEResultType, } from "@/modules/features/SetupWizard/dialogs/setupDialogTypes.ts"; import { syncActivatedRemoteSettings } from "./remoteConfigBuffer.ts"; @@ -116,7 +119,35 @@ export function paneRemoteConfig( .onClick(async () => { const setupManager = this.core.getModule(SetupManager); const originalSettings = getSettingsFromEditingSettings(this.editingSettings); - await setupManager.onlyE2EEConfiguration(UserMode.Update, originalSettings); + const e2eeConf = await setupManager.dialogManager.openWithExplicitCancel< + SetupRemoteE2EEResultType, + EncryptionSettings + >(SetupRemoteE2EE, originalSettings); + if (e2eeConf === "cancelled") { + return; + } + const onlyInternalMetadataPreferenceChanged = + originalSettings.encryptInternalMetadata !== e2eeConf.encryptInternalMetadata && + originalSettings.encrypt === e2eeConf.encrypt && + originalSettings.passphrase === e2eeConf.passphrase && + originalSettings.E2EEAlgorithm === e2eeConf.E2EEAlgorithm && + originalSettings.usePathObfuscation === e2eeConf.usePathObfuscation; + if (onlyInternalMetadataPreferenceChanged) { + await this.services.setting.applyPartial( + { encryptInternalMetadata: e2eeConf.encryptInternalMetadata }, + true + ); + this.editingSettings.encryptInternalMetadata = e2eeConf.encryptInternalMetadata; + if (this.initialSettings) { + this.initialSettings.encryptInternalMetadata = e2eeConf.encryptInternalMetadata; + } + this.requestUpdate(); + } else { + await setupManager.onConfirmApplySettingsFromWizard( + { ...originalSettings, ...e2eeConf }, + UserMode.Update + ); + } updateE2EESummary(); }) .setButtonText("Configure") @@ -243,6 +274,7 @@ export function paneRemoteConfig( ...DEFAULT_SETTINGS, encrypt: this.editingSettings.encrypt, usePathObfuscation: this.editingSettings.usePathObfuscation, + encryptInternalMetadata: this.editingSettings.encryptInternalMetadata, passphrase: this.editingSettings.passphrase, configPassphraseStore: this.editingSettings.configPassphraseStore, }); diff --git a/src/modules/features/SettingDialogue/PaneRemoteConfig.unit.spec.ts b/src/modules/features/SettingDialogue/PaneRemoteConfig.unit.spec.ts index d61a998a..8d259210 100644 --- a/src/modules/features/SettingDialogue/PaneRemoteConfig.unit.spec.ts +++ b/src/modules/features/SettingDialogue/PaneRemoteConfig.unit.spec.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; const runtime = vi.hoisted(() => ({ buttonClasses: [] as string[], + clickHandlers: [] as Array<() => Promise | void>, panels: [] as Array<{ destroy: ReturnType }>, settingClasses: [] as string[], })); @@ -51,7 +52,8 @@ vi.mock("./LiveSyncSetting.ts", () => ({ setDestructive() { return this; }, - onClick() { + onClick(callback: () => Promise | void) { + runtime.clickHandlers.push(callback); return this; }, setButtonText() { @@ -97,6 +99,7 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/common/ConnectionString", () => ({ }, })); vi.mock("@/modules/features/SetupWizard/dialogs/SetupRemote.svelte", () => ({ default: {} })); +vi.mock("@/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte", () => ({ default: {} })); vi.mock("@/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte", () => ({ default: {} })); vi.mock("@/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte", () => ({ default: {} })); vi.mock("@/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte", () => ({ default: {} })); @@ -114,6 +117,7 @@ function createPanelElement(): HTMLElement { afterEach(() => { runtime.buttonClasses.length = 0; + runtime.clickHandlers.length = 0; runtime.panels.length = 0; runtime.settingClasses.length = 0; vi.clearAllMocks(); @@ -148,4 +152,56 @@ describe("paneRemoteConfig", () => { expect(runtime.panels[0].destroy).toHaveBeenCalledOnce(); }); + + it("applies an internal Metadata preference change without scheduling setup initialisation", async () => { + const originalSettings = { + encrypt: true, + passphrase: "passphrase", + E2EEAlgorithm: "v2", + usePathObfuscation: true, + encryptInternalMetadata: false, + remoteConfigurations: {}, + }; + const applyPartial = vi.fn(async () => {}); + const onConfirmApplySettingsFromWizard = vi.fn(async () => {}); + const setupManager = { + dialogManager: { + openWithExplicitCancel: vi.fn(async () => ({ + encrypt: true, + passphrase: "passphrase", + E2EEAlgorithm: "v2", + usePathObfuscation: true, + encryptInternalMetadata: true, + })), + }, + onConfirmApplySettingsFromWizard, + }; + const host = { + editingSettings: { ...originalSettings }, + initialSettings: { ...originalSettings }, + services: { setting: { applyPartial } }, + core: { + settings: { ...originalSettings }, + getModule: vi.fn(() => setupManager), + }, + lifetimeComponent: { register: vi.fn() }, + requestUpdate: vi.fn(), + }; + const addPanel = vi.fn((_parent: HTMLElement, heading: string) => ({ + then(callback: (paneEl: HTMLElement) => void) { + if (heading === "E2EE Configuration") { + callback(createPanelElement()); + } + }, + })); + + paneRemoteConfig.call(host as never, {} as HTMLElement, { addPanel } as never); + await runtime.clickHandlers[0](); + + expect(applyPartial).toHaveBeenCalledWith({ encryptInternalMetadata: true }, true); + expect(onConfirmApplySettingsFromWizard).not.toHaveBeenCalled(); + expect(host.editingSettings.encryptInternalMetadata).toBe(true); + expect(host.initialSettings.encryptInternalMetadata).toBe(true); + expect(host.requestUpdate).toHaveBeenCalledOnce(); + }); }); diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte index 7340f31d..1b02f79c 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte @@ -26,7 +26,8 @@ passphrase: "", E2EEAlgorithm: DEFAULT_SETTINGS.E2EEAlgorithm, usePathObfuscation: true, - } as EncryptionSettings; + encryptInternalMetadata: true, + }; let encryptionSettings = $state({ ...default_encryption }); @@ -42,6 +43,11 @@ if (!encryptionSettings.encrypt) return true; return encryptionSettings.passphrase.trim().length >= 1; }); + let canEncryptInternalMetadata = $derived( + encryptionSettings.encrypt && + encryptionSettings.E2EEAlgorithm === E2EEAlgorithms.V2 && + encryptionSettings.usePathObfuscation + ); function commit() { setResult(pickEncryptionSettings(encryptionSettings)); @@ -87,6 +93,21 @@ {/if} + + + + + This option applies only to CouchDB and requires End-to-End Encryption, the V2 algorithm, and Property Encryption + (Obfuscate Properties). The remote type is selected later in this setup wizard. +
+ It protects Metadata written after the option is enabled; existing Metadata is not rewritten. A manual remote + Rebuild is strongly recommended to protect existing Metadata. +
+ - This option applies only to CouchDB and requires End-to-End Encryption, the V2 algorithm, and Property Encryption + This option encrypts file properties used by Hidden File Sync and Customisation Sync. +
+ It applies only to CouchDB and requires End-to-End Encryption, the V2 algorithm, and Property Encryption (Obfuscate Properties). The remote type is selected later in this setup wizard.
- It protects Metadata written after the option is enabled; existing Metadata is not rewritten. A manual remote - Rebuild is strongly recommended to protect existing Metadata. Update every other synchronising device to a compatible + It protects properties written after the option is enabled; existing properties are not rewritten. A manual remote + Rebuild is strongly recommended to protect existing properties. Update every other synchronising device to a compatible version before enabling this option, including devices currently running LiveSync.
diff --git a/test/e2e-obsidian/scripts/internal-metadata-migration.ts b/test/e2e-obsidian/scripts/internal-metadata-migration.ts index 416dd436..3b45b3af 100644 --- a/test/e2e-obsidian/scripts/internal-metadata-migration.ts +++ b/test/e2e-obsidian/scripts/internal-metadata-migration.ts @@ -164,9 +164,9 @@ async function main(): Promise { .getByRole("button", { name: "Configure", exact: true }) .click(); const dialog = await waitForVisibleObsidianDialogue(navigator.page, "End-to-End Encryption"); - await dialog.getByLabel("Encrypt internal file Metadata", { exact: true }).check(); + await dialog.getByLabel("Encrypt internal file Properties", { exact: true }).check(); await dialog.getByRole("button", { name: "Proceed", exact: true }).click(); - const warning = await waitForVisibleObsidianDialogue(navigator.page, "Encrypt internal file Metadata"); + const warning = await waitForVisibleObsidianDialogue(navigator.page, "Encrypt internal file Properties"); await warning .getByRole("button", { name: "Enable without rebuilding — update every other device first", @@ -221,7 +221,7 @@ async function main(): Promise { await withObsidianPage(session!.remoteDebuggingPort, async (page) => { const dialog = await waitForVisibleObsidianDialogue(page, "Configuration Mismatch Detected"); await dialog - .getByText("Encrypt internal file Metadata", { exact: false }) + .getByText("Encrypt internal file Properties", { exact: false }) .first() .waitFor({ state: "visible" }); await dialog.getByRole("button", { name: "Dismiss", exact: true }).click(); From f954ce6f92c61f2b746fe1bc5bce4f7e6db59cc2 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 27 Sep 2026 16:34:36 +0000 Subject: [PATCH 16/58] Use released Commonlib 0.1.30 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 78086c95..248985a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", - "@vrtmrz/livesync-commonlib": "0.1.29", + "@vrtmrz/livesync-commonlib": "0.1.30", "@vrtmrz/obsidian-plugin-kit": "0.1.4", "@vrtmrz/ui-interactions": "0.1.2", "diff-match-patch": "^1.0.5", @@ -4567,9 +4567,9 @@ } }, "node_modules/@vrtmrz/livesync-commonlib": { - "version": "0.1.29", - "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.29.tgz", - "integrity": "sha512-PeBUUQNSuyS+ISPIvwK3rzaritHi0XDfNlFMOqaRpIbxGAWbWK0YDtE1Htbde3r8Lr3ZdTNjpY+Nmr//OxzgTA==", + "version": "0.1.30", + "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.30.tgz", + "integrity": "sha512-GCVLn/qGGJnJdm9gdM8MigvbE/2tjpgLuekqqr0iPsSOAUrIZNYcWq31KfUwrNsn5L4lLy+aMy0LhyWyBvK6yw==", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.808.0", diff --git a/package.json b/package.json index e66b0717..20d89dd7 100644 --- a/package.json +++ b/package.json @@ -185,7 +185,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", - "@vrtmrz/livesync-commonlib": "0.1.29", + "@vrtmrz/livesync-commonlib": "0.1.30", "@vrtmrz/obsidian-plugin-kit": "0.1.4", "@vrtmrz/ui-interactions": "0.1.2", "diff-match-patch": "^1.0.5", From c1f4376ccc31633f9c3f1758d244bcdcd5156351 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 28 Sep 2026 01:01:25 +0000 Subject: [PATCH 17/58] Explain internal file privacy benefits in release notes --- updates.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/updates.md b/updates.md index 151f8806..cd0c4c1e 100644 --- a/updates.md +++ b/updates.md @@ -16,8 +16,12 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi #### New Feature -- Hidden File Sync and Customisation Sync can now encrypt their paths, times, sizes, and Chunk references in CouchDB Metadata when E2EE V2 and Property Encryption are enabled. The preference is enabled for new Vaults and remains off for existing configurations until selected. It protects future writes; protecting existing Metadata also requires a manual remote Rebuild after all devices have been updated. -- CouchDB records the features its data uses. Clients check these requirements before synchronisation and show any unknown feature identifiers. Receiving an unsupported requirement also stops the active replication. +- We can now keep the file properties used by Hidden File Sync and Customisation Sync private in CouchDB. + - **Encrypt internal file Properties** extends E2EE V2 and Property Encryption to their paths, times, sizes, and Chunk references. + - Existing configurations keep this preference disabled. New Vaults enable it for use when the required encryption settings are active. + - Update every synchronising device before enabling it. It protects future writes; a manual remote Rebuild is strongly recommended to protect existing properties. +- We can now see which unsupported feature prevents a client from synchronising with CouchDB. + - Clients check the features required by the remote before transferring data or resetting the local database for Fast Fetch. Receiving an unsupported requirement also stops active replication. ## 1.0.32 From 126d6eadb858a79a08ad7f600061e54fc8d31196 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 28 Sep 2026 08:09:41 +0000 Subject: [PATCH 18/58] Fix percent-prefixed E2EE passphrase persistence --- package-lock.json | 8 +++--- package.json | 2 +- test/e2e-obsidian/README.md | 2 +- .../scripts/couchdb-manual-setup-workflow.ts | 26 ++++++++++++++++--- updates.md | 5 ++++ 5 files changed, 33 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 248985a4..6b81bcf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", - "@vrtmrz/livesync-commonlib": "0.1.30", + "@vrtmrz/livesync-commonlib": "0.1.31", "@vrtmrz/obsidian-plugin-kit": "0.1.4", "@vrtmrz/ui-interactions": "0.1.2", "diff-match-patch": "^1.0.5", @@ -4567,9 +4567,9 @@ } }, "node_modules/@vrtmrz/livesync-commonlib": { - "version": "0.1.30", - "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.30.tgz", - "integrity": "sha512-GCVLn/qGGJnJdm9gdM8MigvbE/2tjpgLuekqqr0iPsSOAUrIZNYcWq31KfUwrNsn5L4lLy+aMy0LhyWyBvK6yw==", + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.31.tgz", + "integrity": "sha512-ZFspZQmVlsCD41us7OdVc6Fl7T8ckCMiz/BKjif7pE7FO6/vJ4hhEGfcYRSYY+2iyuWyUklJcMgW7nEsO4p2UA==", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.808.0", diff --git a/package.json b/package.json index 20d89dd7..6162c67d 100644 --- a/package.json +++ b/package.json @@ -185,7 +185,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", - "@vrtmrz/livesync-commonlib": "0.1.30", + "@vrtmrz/livesync-commonlib": "0.1.31", "@vrtmrz/obsidian-plugin-kit": "0.1.4", "@vrtmrz/ui-interactions": "0.1.2", "diff-match-patch": "^1.0.5", diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index d557388e..39128c1b 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -137,7 +137,7 @@ The mobile pass uses Obsidian's `app.emulateMobile(true)`, a 390 by 844 CSS-pixe The same workflow checks the two remote-activity status boundaries. It first holds a real CouchDB request at the selected fetch implementation and confirms that `🌐N` is visible while `📲` is absent. It then holds the real one-shot replication immediately before its replicator call, confirms that `📲` is visible while no physical request is active, releases it, and requires the finite and bounded activity counts to return to zero, the request and response counts to balance, and both indicators to disappear. Finally, it creates a remote-only chunk, holds the real on-demand fetch immediately before its remote call, makes the same logical active and idle assertions, and verifies that the fetched chunk is written into the local database. These gates make the active states deterministic without replacing the remote request or operation. -`test:e2e:obsidian:couchdb-manual-setup-workflow` follows the visible onboarding path for the first device when no Setup URI is available. It enters end-to-end encryption and CouchDB details, runs the read-only `Check server requirements` step, requires the prepared fixture to pass without applying a server fix, and lets the onboarding connection test create the named database. After Rebuild completes on the first device, it creates an ordinary note, asks that working device to generate a Setup URI for a second device, completes Fetch there, and verifies a bidirectional note round-trip. The workflow captures each decision point and the expanded server-check result; password controls remain visually masked. +`test:e2e:obsidian:couchdb-manual-setup-workflow` follows the visible onboarding path for the first device when no Setup URI is available. It enters end-to-end encryption and CouchDB details, runs the read-only `Check server requirements` step, requires the prepared fixture to pass without applying a server fix, and lets the onboarding connection test create the named database. After Rebuild completes on the first device, it creates an ordinary note, asks that working device to generate a Setup URI for a second device, completes Fetch there, and verifies a bidirectional note round-trip. The workflow captures each decision point and the expanded server-check result; password controls remain visually masked. It uses an E2EE passphrase beginning with `%`, confirms that the saved settings do not contain it in plain text, and checks that Obsidian restores it after restarting with the first Vault. If this status workflow fails while Obsidian is running, it writes a full-page screenshot and a JSON snapshot of the status text and counters under `/tmp/obsidian-livesync-e2e`. The dialogue-mount workflow leaves desktop and mobile screenshots for both representative Svelte routes, the Hidden File Sync workflow captures the successfully displayed JSON Resolve dialogue before selecting an option, and the Security Seed reconnect workflow captures each significant application state. The suite therefore records representative evidence without capturing every interaction. Set `E2E_OBSIDIAN_DIAGNOSTICS_DIR` to use another directory. diff --git a/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts b/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts index f604059c..4e1945b4 100644 --- a/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts +++ b/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts @@ -39,6 +39,7 @@ process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "90000"; process.env.E2E_OBSIDIAN_COUCHDB_TIMEOUT_MS ??= "30000"; const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETUP_URI_TIMEOUT_MS ?? 30000); +const e2eePassphrase = `%${randomBytes(24).toString("base64url")}`; const notePath = "E2E/manual-couchdb/from-first-device.md"; const noteContent = "# Manual CouchDB setup\n\nThis note was sent by the manually configured first device.\n"; const returnNotePath = "E2E/manual-couchdb/from-second-device.md"; @@ -147,8 +148,7 @@ async function enterManualCouchDBSettings(port: number, couchDb: CouchDbConfig, .locator('input[type="checkbox"]') .first() .check({ timeout: uiTimeoutMs }); - const passphraseValue = randomBytes(24).toString("base64url"); - await passphraseInput.fill(passphraseValue); + await passphraseInput.fill(e2eePassphrase); const passwordToggle = encryption.locator("button.sls-password-toggle"); await passwordToggle.click({ timeout: uiTimeoutMs }); assertEqual( @@ -158,7 +158,7 @@ async function enterManualCouchDBSettings(port: number, couchDb: CouchDbConfig, ); assertEqual( await passphraseInput.inputValue(), - passphraseValue, + e2eePassphrase, "Toggling visibility changed the passphrase value." ); await passwordToggle.click({ timeout: uiTimeoutMs }); @@ -169,7 +169,7 @@ async function enterManualCouchDBSettings(port: number, couchDb: CouchDbConfig, ); assertEqual( await passphraseInput.inputValue(), - passphraseValue, + e2eePassphrase, "Re-masking the passphrase changed its value." ); }); @@ -301,6 +301,23 @@ async function assertPersistedE2EE(vault: TemporaryVault): Promise { if (typeof persisted.encryptedPassphrase !== "string" || persisted.encryptedPassphrase.length === 0) { throw new Error("Manual CouchDB setup did not persist an encrypted E2EE passphrase."); } + if (JSON.stringify(persisted).includes(e2eePassphrase)) { + throw new Error("Manual CouchDB setup persisted the E2EE passphrase in plain text."); + } +} + +async function assertRestoredE2EEPassphrase(session: ObsidianLiveSyncSession, cliBinary: string): Promise { + const restored = await evalObsidianJson( + cliBinary, + [ + "(()=>{", + "const settings=app.plugins.plugins['obsidian-livesync'].core.services.setting.currentSettings();", + `return JSON.stringify(settings.passphrase === ${JSON.stringify(e2eePassphrase)});`, + "})()", + ].join(""), + session.cliEnv + ); + assertEqual(restored, true, "The E2EE passphrase was not restored after Obsidian restarted."); } async function setRemotePreferredE2EEDisabled(context: RunnerContext): Promise { @@ -454,6 +471,7 @@ async function main(): Promise { session = await startUnconfiguredSession(context, vaultA); try { + await assertRestoredE2EEPassphrase(session, context.cliBinary); await scheduleRemoteOverwrite(session.remoteDebuggingPort); screenshots.push(await confirmRebuild(session.remoteDebuggingPort, e2eeRebuildCaptures)); screenshots.push( diff --git a/updates.md b/updates.md index cd0c4c1e..aec7077d 100644 --- a/updates.md +++ b/updates.md @@ -23,6 +23,11 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi - We can now see which unsupported feature prevents a client from synchronising with CouchDB. - Clients check the features required by the remote before transferring data or resetting the local database for Fast Fetch. Receiving an unsupported requirement also stops active replication. +#### Fixed + +- We can now keep using an E2EE passphrase beginning with `%` after restarting Obsidian. (#1221) + - LiveSync encrypts it before saving the settings. If an earlier version saved it in plain text, re-enter the passphrase used to encrypt the existing data after updating. Treat that passphrase as exposed if the affected `data.json` was shared. + ## 1.0.32 27th September, 2026 From 3a478cce3f84454c1f2105f97cc82ba3b4f8f71d Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 28 Sep 2026 10:22:28 +0000 Subject: [PATCH 19/58] Prepare time-bound Setup URI integration for Commonlib release Add Time-bound and Compatible generation to the Obsidian dialogue, browser peer check, and setup tools, with boundary and compatibility coverage. Show the fixed window end before sharing a Time-bound URI. This review branch depends on unpublished @vrtmrz/livesync-commonlib 0.1.32-next.0. The root npm pin and Deno lockfile remain unchanged until that candidate is available; a fresh install of this commit is not yet expected to build. --- docs/design_docs/time_bound_setup_uri.md | 396 ++++++++++++++++++ .../time_bound_setup_uri.probe.mjs | 272 ++++++++++++ docs/setup_object_storage.md | 1 + docs/setup_own_server.md | 3 + docs/setup_p2p.md | 1 + src/apps/cli/commands/runCommand.unit.spec.ts | 32 ++ src/apps/webpeer/src/P2PCheck.svelte | 75 +++- src/apps/webpeer/src/P2PCheckSetup.ts | 19 +- .../dialogues/DialogueToCopy.svelte | 5 + .../setupObsidian/setupProtocol.unit.spec.ts | 31 ++ src/serviceFeatures/setupObsidian/setupUri.ts | 108 ++++- .../setupObsidian/setupUri.unit.spec.ts | 224 ++++++---- test/apps/webpeer/P2PCheckSetup.unit.spec.ts | 15 +- .../webpeer/browser-smoke.test.ts | 38 ++ test/e2e-obsidian/README.md | 2 + test/e2e-obsidian/runner/setupUri.ts | 6 + test/e2e-obsidian/scripts/dialog-mounts.ts | 131 +++++- utils/couchdb/livesync-commonlib.ts | 4 +- utils/livesync-commonlib-version.ts | 2 +- utils/readme.md | 2 + utils/setup/generate_setup_uri.test.ts | 38 ++ utils/setup/generate_setup_uri.ts | 61 ++- utils/setup/livesync-commonlib.ts | 14 +- 23 files changed, 1361 insertions(+), 119 deletions(-) create mode 100644 docs/design_docs/time_bound_setup_uri.md create mode 100644 docs/design_docs/time_bound_setup_uri.probe.mjs diff --git a/docs/design_docs/time_bound_setup_uri.md b/docs/design_docs/time_bound_setup_uri.md new file mode 100644 index 00000000..75fb9131 --- /dev/null +++ b/docs/design_docs/time_bound_setup_uri.md @@ -0,0 +1,396 @@ +--- +date: 2026-09-28 +commonlib-version: "0.1.31" +feasibility-probe-version: "0.1.27" +self-hosted-livesync-version: "1.0.30" +status: implementing +--- + +# Time-bound Setup URIs + +## Purpose and status + +Offer two modes when generating a [Setup URI](../glossary.md): + +- **Ephemeral**, selected by default, derives an effective passphrase from the + entered passphrase and the current fixed time window. +- **Persistent** uses the entered passphrase and the existing URI format + unchanged. It also serves as the compatibility mode for older readers. + +The generation mode choice shows the exact time until which an Ephemeral URI +can be opened through the ordinary reader. Import still requires only the URI and +the entered passphrase. Neither the mode nor a timestamp is stored in the URI. + +The design is technically feasible with the existing encryption primitives. +An executable protocol probe passes 22 cases against Commonlib 0.1.27 and +octagonal-wheels 0.1.54. The implementation is based on Commonlib 0.1.31. +The 0.1.32-next.0 candidate has passed local package and downstream checks; +publication, exact dependency pins, the independent Deno lockfile update, and +mobile performance validation remain outstanding. + +This document records the proposed key derivation and integration contract. The +companion probe demonstrates them without changing an application entry point. + +## Scope + +The first version offers Ephemeral and Persistent only. Ephemeral uses one +fixed seven-day window shared by every implementation. Arbitrary start dates, +custom end dates, selectable window lengths, and rolling seven-day lifetimes +are outside this version. + +Keep the current settings filtering, remote profiles, main and P2P selections, +and short/full export variants. Time binding affects opening the exported +settings; it does not alter Vault encryption, remote credentials, replication, +or settings already imported by another device. + +The existing `settingsQR` format and QR aggregator are separate sharing paths. +They remain outside this design and must not be presented as time-bound. + +## Time window + +Use Unix milliseconds and the following protocol constants: + +```text +windowMilliseconds = 604800000 +windowNumber = floor(nowMilliseconds / windowMilliseconds) +windowStart = windowNumber * windowMilliseconds +usableUntil = (windowNumber + 1) * windowMilliseconds +``` + +The anchor is the Unix epoch, `1970-01-01T00:00:00Z`. Consequently, boundaries +fall on Thursdays at 00:00 UTC. This is a protocol convention, independent of +the device's locale, time zone, daylight-saving rules, and calendar week. +The anchor and window length are part of the derivation profile. Changing them +requires an explicit compatibility design; there is no visible version field +from which an importer can discover a different time condition. + +For example, a URI generated at `2026-09-28T12:00:00Z` belongs to the window +`[2026-09-24T00:00:00Z, 2026-10-01T00:00:00Z)`. Ordinary import rejects it at +the end instant. Generating it one second before that instant leaves one +second of availability. The UI must not describe this as seven days from +generation. + +Use safe, non-negative integer timestamps without 32-bit coercion. The +reader tries only its current window. There is no previous-window allowance, +future-window search, modulo wraparound, or grace period. Devices whose clocks +fall on different sides of a boundary can disagree; local date formatting +does not affect the calculation. + +## Passphrase derivation and encryption + +The time factor follows the counter construction used by +[TOTP, RFC 6238 section 4.2](https://www.rfc-editor.org/rfc/rfc6238.html#section-4.2). +This is a time-bound encryption format, not a TOTP authentication protocol: +there is no six-digit code, trusted validation server, or single-use state. + +Reuse Commonlib's current `encryptString` and `decryptString` path. It uses +octagonal-wheels' salted PBKDF2/HKDF and AES-GCM implementation. Persistent +passes the entered passphrase directly to that path. Only Ephemeral applies +a full-length transformation: + +```text +if mode == "persistent": + effectivePassphrase = enteredPassphrase +else: + hmacKey = SHA-256(UTF-8(enteredPassphrase)) + context = JSON.stringify([ + "livesync/setup-uri", + "tb1", + "ephemeral", + windowNumber + ]) + effectivePassphrase = lowercaseHex(HMAC-SHA-256(hmacKey, UTF-8(context))) +encryptedSettings = encryptString(preparedSettingsJSON, effectivePassphrase) +``` + +The mode strings are exactly `ephemeral` and `persistent`. JSON encoding, +field order, lowercase hexadecimal, and the domain/profile strings are part +of the proposed Ephemeral derivation. `tb1` is an internal derivation identifier; +it is not a URI prefix or stored field. Persistent applies no new transformation. + +The initial SHA-256 provides a fixed-length HMAC key, including for an empty +low-level input. It does not replace password stretching: the existing +encryption function still performs PBKDF2. Do not trim or normalise the +entered passphrase within this transformation. Existing host requirements for +a non-empty passphrase remain in force; this design adds no new password +policy. + +At the assessed dependency versions, the encryption path uses PBKDF2-SHA-256 +with 310,000 iterations, HKDF-SHA-256, and AES-256-GCM with a 128-bit tag. Its +`%$` payload includes the material needed for decryption. The PBKDF2 salt may +be reused within a session; the IV and HKDF salt are generated for each +encryption. Retain these existing semantics rather than claiming a new +per-URI PBKDF2 salt or introducing another encryption implementation. + +For the synthetic passphrase `test-passphrase`, fixed derivation vectors are: + +```text +context: ["livesync/setup-uri","tb1","ephemeral",1234] +effectivePassphrase: b39361c51f0b7bd835554db1dffbc9a540fb30789aa62bf53e39e07d1073013b + +mode: persistent +effectivePassphrase: test-passphrase +``` + +The probe checks the Ephemeral Web Crypto output against a value calculated +separately through Node's SHA-256 and HMAC interface, and checks that Persistent +retains the exact input. Encryption remains randomised; these vectors specify +the effective passphrase, not the complete URI. + +This composition needs Commonlib changes but no new cryptographic dependency +or remote service. The prototype verifies interoperability with the existing +encryption path; it is not an independent cryptographic audit. + +## URI format and decoding + +Keep the existing URI prefix and encrypted representation without a new marker: + +```text +obsidian://setuplivesync?settings= +``` + +Both modes use the existing `%$` encrypted representation. There is no mode +flag, timestamp, window number, duration, derivation-version marker, or separate +query parameter. For identical prepared settings, both modes have the same +binary layout and ciphertext length. Percent-encoded text length may vary with +the random ciphertext; equal URI text length is not a requirement. + +Persistent delegates to the existing encoder with the entered passphrase and +the same settings-filtering options. A new marker or a transformed Persistent +passphrase would defeat older readers. Ephemeral uses the same external format +to preserve mode hiding. The protocol handler continues to reconstruct the URI +from the existing `settings` query value. + +For a `%$` payload, including an existing URI made before this feature, the +updated decoder: + +1. validates the envelope and captures the current window; +2. derives the Ephemeral candidate for that window and takes the entered + passphrase unchanged as the Persistent candidate; +3. attempts authenticated decryption with both candidates, without returning + early after the first success; +4. checks the current window again before returning an Ephemeral result; and +5. returns settings only if exactly one candidate is accepted. + +Keep expected candidate failures internal. Two failed candidates produce one +generic opening failure. A wrong passphrase, an out-of-window Ephemeral URI, +and damaged authenticated ciphertext do not receive different user messages. +There is no information from which to report a definite historical end time +for an unreadable URI. + +The raw-passphrase attempt is an intentional part of the two-candidate reader. +An Ephemeral URI from another window still fails both candidates with the +entered passphrase; this attempt does not remove its time condition. There is +no plaintext recovery, time-window search, or additional key fallback. + +Older encryption representations other than `%$` retain their existing +legacy-only decoder and passphrase handling. Unsupported prefixes remain +unsupported. Malformed `%$` payloads fail authentication with both candidates. +Bound time-bound candidate work to two attempts; the existing decoder continues +to own any historical encryption-format compatibility trials. + +Because the derivation profile is not stored, an older reader cannot identify +an Ephemeral URI and give an upgrade-specific error. It reports a decryption +failure. A future change to the Ephemeral profile must define its bounded +candidate policy explicitly; the reader must not infer arbitrary profiles or +scan them without a limit. + +Two trials make the amount of cryptographic work predictable, but do not +constitute a constant-time implementation. This design does not claim to hide +the mode from a caller who can instrument the decoder and knows the passphrase. + +## Generation and import interaction + +Keep the existing passphrase prompt. After it, use the existing confirmation +dialogue to choose Time-bound (the Ephemeral encoder mode) or Compatible (the +Persistent encoder mode). Select Time-bound by default, show its exact absolute +end in the device's local time zone with its time-zone name or UTC offset, and +explain that this is the end of a fixed UTC window rather than seven days from +generation. Compatible has no time condition and remains readable by older +clients. No synchronised setting or permanent Vault preference is needed. + +Commonlib exposes the current window end for this pre-generation choice. +Compare it with the `usableUntil` returned alongside the generated URI. If +the window changed during selection or encryption, show a fresh choice with +the new end before opening the existing copy dialogue. Persistent generation +does not consult the clock and returns `null` for `usableUntil`. + +The existing copy dialogue does not monitor the clock. If it stays open across +the boundary, it can still display and copy a URI whose window has ended; +the reader will reject that URI. The end time was shown at the mode choice. +The copy dialogue does not claim a rolling week or silently switch to +Compatible. + +Import requires no mode selector or date input. Recheck the window at the +end of decryption so that an Ephemeral operation crossing the boundary does +not return settings. Once settings have been returned successfully, subsequent +confirmation, setup, and synchronisation do not remain time-bound: the URI +has already been opened. This is an import boundary, not remote revocation. + +## Security and privacy boundary + +The hidden mode is an external-format property. A passive holder of the URI +does not receive an explicit time condition or Persistent marker. Randomised +encryption and the unchanged prepared settings representation avoid adding +a mode-dependent field or length difference. + +The clock and reader are controlled by the receiving device. Anyone with the +correct passphrase can reproduce historical candidates, modify the reader, +or change the supplied clock. Including Persistent adds one possible +candidate; it does not make a small calendar search cryptographically hard. +Time binding adds no independent secret and is not a substitute for a strong +Setup URI passphrase. + +The same URI can be opened repeatedly within its window. A replayed clock +earlier than generation, but inside that same window, also derives the same +key. Successfully copied settings and remote credentials remain usable after +the window ends, subject to their own remote policies. + +No URI, entered or effective passphrase, derived key, decrypted settings, or +candidate-specific authentication result should enter ordinary logs or reports. +The generation screen necessarily reveals the selected mode and end time to +its user; hiding information in the URI does not hide that screen or a +separately shared message. + +## Ownership and compatibility + +Commonlib owns preparation of exported settings, the time-window calculation, +passphrase derivation, format parsing, and dual-candidate decoding. Make these +changes in the Commonlib repository. LiveSync must consume a validated packed +artefact and then an exact reviewed package version, following +[the Commonlib dependency workflow](../../devs.md#commonlib-dependency). + +Preserve the existing positional encoder API. Its third and fourth arguments +already mean properties to remove and whether to omit default values. Add a +separate encoder, provisionally `encodeTimeBoundSetupURI`, with an options +object containing the mode and the existing export options. Its result is: + +```typescript +type TimeBoundSetupURIResult = { + uri: string; + usableUntil: number | null; +}; +``` + +The new encoder defaults to Ephemeral. Preserve the old encoder's legacy +behaviour for callers which have not migrated. Extend +`decodeSettingsFromSetupURI` to try both candidates for `%$` while retaining its +settings return contract and historical format handling. Persistent generation +through the new API delegates to the old encoder. Time injection belongs in +internal test seams, not an import option exposed to users. + +| Surface | Required work | +| ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Obsidian generation](../../src/serviceFeatures/setupObsidian/setupUri.ts) | Reuse the passphrase and copy dialogues, add a mode choice with the exact current window end, and use the new encoder for all three copy variants and the copy event. Preserve each variant's export filters. | +| [Obsidian import](../../src/modules/features/SetupWizard/dialogs/UseSetupURI.svelte) | Consume the updated decoder and retain one authentication failure message. The existing prefix check indicates URI shape, not successful authentication. | +| [Protocol handler](../../src/serviceFeatures/setupObsidian/setupProtocol.ts) | Keep the existing `settings` reconstruction; test encoded delimiters for both modes. | +| [CLI setup](../../src/apps/cli/commands/runCommand.ts) | Consume the updated decoder and leave settings unchanged after any rejection. | +| [WebPeer generator](../../src/apps/webpeer/src/P2PCheckSetup.ts) | Choose a mode explicitly and return availability to its result UI; keep Ephemeral as the default. | +| [Setup utility](../../utils/setup/generate_setup_uri.ts) | Add an explicit mode input, default to Ephemeral, and return/print the end time. Persistent is an explicit choice for durable provisioning output. | +| [Setup utility package facade](../../utils/setup/livesync-commonlib.ts) | Update its independent Commonlib pin and associated lockfiles; updating the root npm dependency alone is insufficient. | + +The Fly.io wrapper delegates to the setup utility and should inherit the same +contract. All maintained generators must migrate explicitly before this +feature is described as the default across applications. Non-interactive +automation which needs a durable URI must select Persistent. + +WebPeer hides an Ephemeral URI and disables its copy and additional-device +actions after the window ends. Its connection monitor remains available for a +device that imported the URI before the end; opening the URI does not expire +the imported P2P credentials. + +| URI | Updated reader | Reader without time-bound support | +| -------------- | ------------------------------------------------------- | --------------------------------- | +| Legacy | Existing behaviour, without a time condition | Existing behaviour | +| New Ephemeral | Opens in the current window with the correct passphrase | Rejected | +| New Persistent | Opens with the correct passphrase | Existing behaviour | + +The assessed, unchanged Commonlib 0.1.27 decoder opens Persistent with the +entered passphrase and rejects Ephemeral with that same passphrase. Persistent +compatibility means compatibility with readers which already support the +ordinary generated `%$` representation; it does not add that representation +to still older readers. Historical supported versions and their settings +schemas still need compatibility fixtures before rollout. + +Keep the original raw-passphrase behaviour of existing URIs. They are +indistinguishable from newly generated Persistent URIs to the updated reader. +The previous marked-envelope sketch rejected an added Persistent compatibility +case with `Unsupported encryption format`. The same synthetic settings and +entered passphrase now open through the unchanged decoder, while separate +tests retain old-reader rejection of Ephemeral and rejection outside its window. + +## Feasibility evidence and validation + +The [executable design probe](time_bound_setup_uri.probe.mjs) uses Web Crypto +for the proposed transformation and the actual Commonlib encoder/decoder for +settings filtering and encryption. It is an isolated specification exercise; +it is not imported by an application or registered in the production test suite. + +Run it after installing the dependency versions assessed by this document: + +```sh +NODE_OPTIONS=--max-old-space-size=512 node docs/design_docs/time_bound_setup_uri.probe.mjs +``` + +Alternatively, pass the directory of an unpacked Commonlib 0.1.27 package as +the first argument. Its module resolution must supply octagonal-wheels 0.1.54 +and the remaining declared dependencies. The probe verifies the Commonlib +version before running; it does not install or download packages. + +The assessment used Node.js 24.18.0 and registry artefacts for Commonlib 0.1.27 +and octagonal-wheels 0.1.54, verified against the lockfile's SHA-512 values. +All 22 cases passed: + +- fixed Ephemeral derivation checked through Web Crypto and Node HMAC, and an + unchanged Persistent passphrase; +- current-window success, previous/later-window rejection, and exact end; +- Persistent across distant timestamps and both candidate attempts on success; +- wrong-passphrase rejection and a shared external format; +- stable end-time metadata and randomised repeated encryption; +- legacy import, old-reader acceptance of Persistent, and old-reader rejection + of Ephemeral; +- unsupported-prefix, malformed-ciphertext, and ciphertext-tampering rejection; +- Persistent generation without reading the clock; +- protocol query decoding/re-encoding; +- generation and import crossing a boundary; +- equivalent timestamps expressed with different UTC offsets; +- Unicode, whitespace, empty low-level passphrase input, and distinct Ephemeral + and raw-passphrase candidates; +- the deliberate limitation that clock rollback reproduces an old key. + +This demonstrates the proposed mechanism and baseline interoperability, not +completed application support, formal mode indistinguishability, or a mobile +latency bound. The present encryption API may perform two PBKDF2 derivations +on a cold import. Do not infer production performance from this small probe, +whose operations can reuse the dependency's in-memory key cache. + +Implementation validation must cover Commonlib unit and packed-package tests, +plus shared fixed-input protocol vectors across consumers. Add targeted LiveSync +tests for the generation metadata, cancellation, a window change during mode +selection, encoded deep links, and rejected imports without settings writes. Extend CLI, WebPeer, +and Deno setup-tool round trips, including their Persistent selection. + +Then run the required LiveSync checks, unit suite, and production build, one +broad process at a time with `NODE_OPTIONS=--max-old-space-size=3072` and bounded +test workers. Use the existing focused real-Obsidian Setup URI workflow for +copy/paste, deep-link import, cancellation, and successful setup. Verify +selection-boundary handling with an injected test clock and mobile behaviour on a +supported runtime; do not change the host's system clock. No remote database +service is needed for the protocol tests themselves. + +There is no identified architectural blocker. The 0.1.31-based Commonlib +candidate passes its type, unit, boundary, release-process, and packed-package +gates. LiveSync passes source checks, the unit suite, the Obsidian plug-in +build, and the WebPeer and CLI builds against that local tarball. The complete +unit suite passed with subprocess permissions needed by its CLI installer +fixtures. Focused real-Obsidian generation checks passed on desktop and +emulated mobile, including the displayed Time-bound end, Compatible output, +and mobile touch targets. Unit and protocol tests cover window boundaries and +preserve the existing distinction between an empty passphrase and cancellation. +The WebPeer browser test confirms that its monitor remains available after the +URI window ends for a device that has already imported the URI. +The Deno generator passed against the same local packed +candidate through a temporary import map. These results do not establish a +mobile performance bound or replace validation of the published package and +its lockfiles. The first implementation changes only primary-language +resources; translation changes require separate scope. diff --git a/docs/design_docs/time_bound_setup_uri.probe.mjs b/docs/design_docs/time_bound_setup_uri.probe.mjs new file mode 100644 index 00000000..d26a1143 --- /dev/null +++ b/docs/design_docs/time_bound_setup_uri.probe.mjs @@ -0,0 +1,272 @@ +// Executable design probe; this file is not part of any application bundle. +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const packageDirectory = process.argv[2] + ? resolve(process.argv[2]) + : fileURLToPath(new URL(".", import.meta.resolve("@vrtmrz/livesync-commonlib/package.json"))); +const packageURL = pathToFileURL(`${packageDirectory}/`); +const metadata = JSON.parse(await readFile(new URL("package.json", packageURL), "utf8")); +assert.equal(metadata.version, "0.1.27", "Run against the Commonlib version assessed by this design"); +const { encodeSettingsToSetupURI: encodeLegacy, decodeSettingsFromSetupURI: decodeLegacy } = await import( + new URL("dist/API/processSetting.js", packageURL).href +); +const { configURIBase } = await import(new URL("dist/common/types.js", packageURL).href); + +const WEEK_MS = 604_800_000; +const passphrase = "synthetic time-bound URI passphrase"; +const settings = { + couchDB_URI: "https://example.invalid", + couchDB_USER: "synthetic-user", + couchDB_PASSWORD: "synthetic-secret", + isConfigured: true, +}; +const midweek = Date.parse("2026-09-28T12:00:00Z"); +const slot = (now) => { + assert.ok(Number.isSafeInteger(now) && now >= 0, "A supported UTC timestamp is required"); + return Math.floor(now / WEEK_MS); +}; +const end = (slot(midweek) + 1) * WEEK_MS; +const payloadOf = (uri) => { + assert.ok(uri.trim().startsWith(configURIBase)); + return decodeURIComponent(uri.trim().slice(configURIBase.length)); +}; +const wrap = (payload) => configURIBase + encodeURIComponent(payload); +const fixedClock = (now) => () => now; + +async function effectivePassphrase(secret, mode, bucket) { + if (mode === "persistent") return secret; + assert.equal(mode, "ephemeral"); + const text = new TextEncoder(); + const keyBytes = await crypto.subtle.digest("SHA-256", text.encode(secret)); + const key = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); + const context = JSON.stringify(["livesync/setup-uri", "tb1", "ephemeral", bucket]); + const signed = await crypto.subtle.sign("HMAC", key, text.encode(context)); + return Array.from(new Uint8Array(signed), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +async function encode(mode, clock, secret = passphrase) { + assert.ok(mode === "ephemeral" || mode === "persistent"); + if (mode === "persistent") { + return { uri: (await encodeLegacy(settings, secret)).trim(), usableUntil: null }; + } + const bucket = slot(clock()); + const effective = await effectivePassphrase(secret, mode, bucket); + const legacy = await encodeLegacy(settings, effective); + if (mode === "ephemeral" && slot(clock()) !== bucket) throw new Error("Window changed"); + return { + uri: legacy.trim(), + usableUntil: (bucket + 1) * WEEK_MS, + }; +} + +async function decode(uri, secret, clock, attempts = []) { + const payload = payloadOf(uri); + if (!payload.startsWith("%$")) { + return decodeLegacy(uri.trim(), secret); + } + const bucket = slot(clock()); + const authenticated = []; + for (const mode of ["ephemeral", "persistent"]) { + const effective = await effectivePassphrase(secret, mode, bucket); + attempts.push(mode); + try { + const value = await decodeLegacy(uri.trim(), effective); + if (value !== false) authenticated.push({ mode, value }); + } catch { + // Authentication failures are expected while trying the two candidates. + } + } + const currentBucket = slot(clock()); + const accepted = authenticated.filter(({ mode }) => mode === "persistent" || bucket === currentBucket); + if (accepted.length !== 1) throw new Error("Cannot open Setup URI"); + return accepted[0].value; +} + +const ephemeral = await encode("ephemeral", fixedClock(midweek)); +const persistent = await encode("persistent", fixedClock(midweek)); + +await test("Persistent opens in the unchanged legacy decoder with the entered passphrase", async () => { + assert.equal((await decodeLegacy(persistent.uri, passphrase)).couchDB_PASSWORD, settings.couchDB_PASSWORD); +}); + +await test("Web Crypto derivation matches fixed vectors computed through Node HMAC", async () => { + assert.equal( + await effectivePassphrase("test-passphrase", "ephemeral", 1234), + "b39361c51f0b7bd835554db1dffbc9a540fb30789aa62bf53e39e07d1073013b" + ); + assert.equal(await effectivePassphrase("test-passphrase", "persistent", 1234), "test-passphrase"); +}); + +await test("Ephemeral accepts the entire current UTC bucket, including before creation", async () => { + for (const now of [slot(midweek) * WEEK_MS, midweek, end - 1]) { + const attempts = []; + assert.equal( + (await decode(ephemeral.uri, passphrase, fixedClock(now), attempts)).couchDB_PASSWORD, + settings.couchDB_PASSWORD + ); + assert.deepEqual(attempts, ["ephemeral", "persistent"]); + } +}); + +await test("Ephemeral rejects the previous bucket, the exact end, and later buckets", async () => { + for (const now of [slot(midweek) * WEEK_MS - 1, end, end + WEEK_MS, end + 100 * WEEK_MS]) { + const attempts = []; + await assert.rejects(decode(ephemeral.uri, passphrase, fixedClock(now), attempts), /Cannot open/); + assert.deepEqual(attempts, ["ephemeral", "persistent"]); + } +}); + +await test("Persistent accepts distant supported timestamps using the same two attempts", async () => { + for (const now of [0, midweek, end, end + 100 * WEEK_MS]) { + const attempts = []; + assert.equal( + (await decode(persistent.uri, passphrase, fixedClock(now), attempts)).couchDB_PASSWORD, + settings.couchDB_PASSWORD + ); + assert.deepEqual(attempts, ["ephemeral", "persistent"]); + } +}); + +await test("Both modes reject an incorrect passphrase with the same final error", async () => { + for (const generated of [ephemeral, persistent]) { + await assert.rejects(decode(generated.uri, "incorrect", fixedClock(midweek)), /Cannot open Setup URI/); + } +}); + +await test("Generation metadata agrees with the fixed UTC boundary", () => { + assert.equal(ephemeral.usableUntil, end); + assert.equal(new Date(end).toISOString(), "2026-10-01T00:00:00.000Z"); + assert.equal(persistent.usableUntil, null); +}); + +await test("Both modes retain the legacy binary layout without a new marker or timestamp field", () => { + const binaryLength = (uri) => { + const payload = payloadOf(uri); + assert.ok(payload.startsWith("%$")); + return Buffer.from(payload.slice("%$".length), "base64").length; + }; + assert.equal(binaryLength(ephemeral.uri), binaryLength(persistent.uri)); +}); + +await test("Repeated generation has different ciphertext without changing the time limit", async () => { + const again = await encode("ephemeral", fixedClock(midweek)); + assert.notEqual(again.uri, ephemeral.uri); + assert.equal(again.usableUntil, ephemeral.usableUntil); +}); + +await test("Old decoder cannot open Ephemeral with the entered passphrase", async () => { + await assert.rejects(decodeLegacy(ephemeral.uri, passphrase)); +}); + +await test("Legacy URI remains readable without time binding", async () => { + const legacy = await encodeLegacy(settings, passphrase); + const attempts = []; + const value = await decode(legacy, passphrase, fixedClock(end + 100 * WEEK_MS), attempts); + assert.equal(value.couchDB_PASSWORD, settings.couchDB_PASSWORD); + assert.deepEqual(attempts, ["ephemeral", "persistent"]); +}); + +await test("Unsupported prefixes are rejected without time-bound trials", async () => { + for (const payload of ["tb1:" + payloadOf(ephemeral.uri), "tb2:", "", "unlimited"]) { + const attempts = []; + await assert.rejects(decode(wrap(payload), passphrase, fixedClock(midweek), attempts), /format/); + assert.deepEqual(attempts, []); + } +}); + +await test("Missing or truncated legacy-format ciphertext fails both candidates", async () => { + for (const payload of ["%$", "%$AA=="]) { + const attempts = []; + await assert.rejects(decode(wrap(payload), passphrase, fixedClock(midweek), attempts), /Cannot open/); + assert.deepEqual(attempts, ["ephemeral", "persistent"]); + } +}); + +await test("Persistent generation does not consult the clock", async () => { + const generated = await encode("persistent", () => { + throw new Error("The Persistent generator must not read time"); + }); + assert.equal(generated.usableUntil, null); + assert.equal((await decodeLegacy(generated.uri, passphrase)).couchDB_PASSWORD, settings.couchDB_PASSWORD); +}); + +await test("Changing authenticated ciphertext is rejected", async () => { + const payload = payloadOf(ephemeral.uri); + const encoded = payload.slice("%$".length); + const bytes = Buffer.from(encoded, "base64"); + bytes[bytes.length - 1] ^= 1; + await assert.rejects(decode(wrap("%$" + bytes.toString("base64")), passphrase, fixedClock(midweek)), /Cannot open/); +}); + +await test("Protocol query decoding and re-encoding preserve the new payload", async () => { + for (const generated of [ephemeral, persistent]) { + const incomingSettings = new URL(generated.uri).searchParams.get("settings"); + const reconstructed = configURIBase + encodeURIComponent(incomingSettings); + assert.equal( + (await decode(reconstructed, passphrase, fixedClock(midweek))).couchDB_PASSWORD, + settings.couchDB_PASSWORD + ); + } +}); + +await test("Generation crossing the boundary withholds an Ephemeral result", async () => { + let reads = 0; + await assert.rejects( + encode("ephemeral", () => (reads++ === 0 ? end - 1 : end)), + /Window changed/ + ); +}); + +await test("Import crossing the boundary withholds Ephemeral settings but accepts Persistent", async () => { + for (const [generated, succeeds] of [ + [ephemeral, false], + [persistent, true], + ]) { + let reads = 0; + const result = decode(generated.uri, passphrase, () => (reads++ === 0 ? end - 1 : end)); + if (succeeds) assert.equal((await result).couchDB_PASSWORD, settings.couchDB_PASSWORD); + else await assert.rejects(result, /Cannot open/); + } +}); + +await test("Explicit UTC offsets representing the same instant select the same bucket", async () => { + for (const date of ["2026-09-28T12:00:00Z", "2026-09-28T21:00:00+09:00", "2026-09-28T05:00:00-07:00"]) { + assert.equal( + (await decode(ephemeral.uri, passphrase, fixedClock(Date.parse(date)))).couchDB_PASSWORD, + settings.couchDB_PASSWORD + ); + } +}); + +await test("Passphrase transformation preserves Unicode and supports an empty low-level input", async () => { + for (const secret of ["", "合言葉🔑é", "e\u0301", " leading and trailing "]) { + const generated = await encode("ephemeral", fixedClock(midweek), secret); + assert.equal( + (await decode(generated.uri, secret, fixedClock(midweek))).couchDB_PASSWORD, + settings.couchDB_PASSWORD + ); + } + assert.notEqual( + await effectivePassphrase("é", "persistent", 0), + await effectivePassphrase("e\u0301", "persistent", 0) + ); +}); + +await test("Ephemeral derivation differs from the raw Persistent passphrase even for bucket zero", async () => { + assert.notEqual( + await effectivePassphrase(passphrase, "ephemeral", 0), + await effectivePassphrase(passphrase, "persistent", 0) + ); +}); + +await test("Clock rollback reproduces an old Ephemeral key", async () => { + await assert.rejects(decode(ephemeral.uri, passphrase, fixedClock(end)), /Cannot open/); + assert.equal( + (await decode(ephemeral.uri, passphrase, fixedClock(midweek))).couchDB_PASSWORD, + settings.couchDB_PASSWORD + ); +}); diff --git a/docs/setup_object_storage.md b/docs/setup_object_storage.md index 0244a766..4c67af1c 100644 --- a/docs/setup_object_storage.md +++ b/docs/setup_object_storage.md @@ -32,6 +32,7 @@ deno run --minimum-dependency-age=0 --allow-env https://raw.githubusercontent.co For providers which require them, set `force_path_style`, `use_custom_request_handler`, or `bucket_custom_headers` as described in the [setup utility reference](../utils/readme.md#object-storage). Store the generated Setup URI and Setup URI passphrase separately. The URI is encrypted, but it contains the Object Storage credentials. +The generator prints the exact end time of its default Ephemeral URI. Set `uri_mode=persistent` before running it if the URI must remain usable without a time condition. ## Set up the first device diff --git a/docs/setup_own_server.md b/docs/setup_own_server.md index 0eac36b2..05ad2bf4 100644 --- a/docs/setup_own_server.md +++ b/docs/setup_own_server.md @@ -192,10 +192,13 @@ deno run --minimum-dependency-age=0 --allow-env https://raw.githubusercontent.co The generator consumes the exact registry-pinned Commonlib release used by the provisioning utility. It creates a configured CouchDB remote profile, applies the current defaults for a new Vault, and encodes them with Commonlib's Setup URI contract. +By default, the URI is Ephemeral and can be opened only until the exact UTC time printed by the generator. This is the end of a fixed seven-day window, not seven days after creation. Set `uri_mode=persistent` before running the command if the URI needs no time condition or must be readable by an older client that supports the existing encrypted format. + You will then get the following output: ```bash Generated couchdb Setup URI. +Ephemeral: usable until 2026-10-01T00:00:00.000Z (UTC). Your passphrase for the Setup URI is: H7vX...a-random-32-character-value This passphrase is never shown again, so store it safely. obsidian://setuplivesync?settings=%5B%22tm2DpsOE74nJAryprZO2M93wF%2Fvg.......4b26ed33230729%22%5D diff --git a/docs/setup_p2p.md b/docs/setup_p2p.md index fb13a9c1..20b949d3 100644 --- a/docs/setup_p2p.md +++ b/docs/setup_p2p.md @@ -136,3 +136,4 @@ deno run --minimum-dependency-age=0 --allow-env https://raw.githubusercontent.co ``` The generated Setup URI contains the encrypted room, relay, and Vault settings. It deliberately omits the device-specific name. Store the URI and its passphrase separately. After importing it on the first device, continue from the initialisation step above, then generate a fresh Setup URI for an additional device from that working device. +The generator prints the exact end time of its default Ephemeral URI. Set `uri_mode=persistent` before running it if the URI must remain usable without a time condition. diff --git a/src/apps/cli/commands/runCommand.unit.spec.ts b/src/apps/cli/commands/runCommand.unit.spec.ts index f4d3a45a..61d253a1 100644 --- a/src/apps/cli/commands/runCommand.unit.spec.ts +++ b/src/apps/cli/commands/runCommand.unit.spec.ts @@ -419,6 +419,38 @@ describe("runCommand abnormal cases", () => { expect(appliedSettings.useIndexedDBAdapter).toBe(false); }); + it("setup opens Ephemeral in its window and leaves settings untouched afterwards", async () => { + const end = Date.parse("2026-10-01T00:00:00Z"); + const clock = vi.spyOn(Date, "now").mockReturnValue(end - 1_000); + const passphrase = "time-bound-passphrase"; + try { + const { uri, usableUntil } = await processSetting.encodeTimeBoundSetupURI( + { ...DEFAULT_SETTINGS, isConfigured: true, couchDB_DBNAME: "time-bound-vault" }, + passphrase + ); + expect(usableUntil).toBe(end); + + const inWindow = createCoreMock(); + inWindow.services.context.standardIo.prompt.mockResolvedValue(passphrase); + await runCommand(makeOptions("setup", [uri.trim()]), { ...context, core: inWindow }); + expect(inWindow.services.setting.applyExternalSettings).toHaveBeenCalledWith( + expect.objectContaining({ couchDB_DBNAME: "time-bound-vault" }), + true + ); + + clock.mockReturnValue(end); + const outOfWindow = createCoreMock(); + outOfWindow.services.context.standardIo.prompt.mockResolvedValue(passphrase); + await expect(runCommand(makeOptions("setup", [uri.trim()]), { ...context, core: outOfWindow })).rejects.toThrow( + "Cannot open Setup URI" + ); + expect(outOfWindow.services.setting.applyExternalSettings).not.toHaveBeenCalled(); + expect(outOfWindow.services.control.applySettings).not.toHaveBeenCalled(); + } finally { + clock.mockRestore(); + } + }); + it("setup imports managed TURN through the existing encrypted URI", async () => { const core = createCoreMock(); const profiles = { diff --git a/src/apps/webpeer/src/P2PCheck.svelte b/src/apps/webpeer/src/P2PCheck.svelte index 670b306f..151cc1c4 100644 --- a/src/apps/webpeer/src/P2PCheck.svelte +++ b/src/apps/webpeer/src/P2PCheck.svelte @@ -1,7 +1,8 @@ @@ -58,7 +158,11 @@ {translateMessage("Please configure your end-to-end encryption settings.")} - + toggleEncryption(event.currentTarget.checked)} + /> {translateMessage( @@ -93,6 +197,164 @@ {/if} +
+ {translateMessage("ID generation")} + + + +
+ {#if encryptionSettings.encrypt && idConfigurationChoice === "keep" && !idDerivationConfigured} + + {translateMessage("Changing the E2EE passphrase changes IDs generated by the legacy configuration.")} + + {/if} + {#if (encryptionSettings.encrypt && idConfigurationChoice !== "keep") || idDerivationConfigured} + {#if encryptionSettings.encrypt} + + {translateMessage( + "This uses a saved key for new Chunk IDs and obfuscated Metadata document IDs, so changing the E2EE passphrase does not derive a new key automatically." + )} + + {/if} + {#if idDerivationConfigured} + + {translateMessage("The saved ID key is configured. Its source cannot be shown again.")} + + + {#if recoveryCodeVisible} + + + + + {#if recoveryCodeCopied} + {translateMessage("Recovery code copied.")} + {/if} + {/if} + {/if} + {#if encryptionSettings.encrypt} + {#if idConfigurationChoice === "custom"} +
+ {translateMessage("How to set the ID key")} + + + +
+ {#if idCustomChoice === "source" || idCustomChoice === "recovery"} + + + + {/if} + {/if} + {#if idDerivationConfigured && idConfigurationChoice !== "keep"} + + {translateMessage("The displayed recovery code belongs to the current key. Reopen this dialogue after saving to copy the replacement key.")} + + {/if} + {#if idConfigurationChoice === "custom" && idCustomChoice === "source"} + + {translateMessage("Choose a long, unpredictable source. It is used once and cannot be shown again after saving. A recovery code can be displayed on this device later. This input also accepts a tagged recovery code.")} + + {:else if idConfigurationChoice === "custom" && idCustomChoice === "recovery"} + + {translateMessage("Paste a tagged recovery code from an existing device to restore the same ID key.")} + + {:else if idConfigurationChoice === "random"} + + {translateMessage("For recovery after losing every device, save the recovery code after setup or choose an ID source you can reproduce.")} + + {:else if idConfigurationChoice === "custom" && idCustomChoice === "passphrase"} + + {translateMessage( + "The ID key is derived from the current E2EE passphrase and saved separately. Changing that passphrase later does not change the saved ID key. To reduce the risk of guessing that passphrase from known IDs, use a separate, unpredictable ID source instead." + )} + + {/if} + {#if idDerivationConfigured && idConfigurationChoice === "custom" && idCustomChoice !== "passphrase"} + {translateMessage("Leave this input empty to keep the saved ID key.")} + {/if} + {/if} + {idDerivationError} + {/if} + diff --git a/src/modules/features/SetupWizard/dialogs/setupDialogTypes.ts b/src/modules/features/SetupWizard/dialogs/setupDialogTypes.ts index 2c9ef58e..816fc8c9 100644 --- a/src/modules/features/SetupWizard/dialogs/setupDialogTypes.ts +++ b/src/modules/features/SetupWizard/dialogs/setupDialogTypes.ts @@ -110,6 +110,10 @@ export type SetupRemoteResultType = typeof TYPE_COUCHDB | typeof TYPE_BUCKET | t export type UseSetupURIResultType = typeof TYPE_CANCELLED | ObsidianLiveSyncSettings; export type SetupRemoteE2EEResultType = typeof TYPE_CANCELLED | EncryptionSettings; +export type SetupRemoteE2EEInitialData = { + settings: EncryptionSettings; + newVault: boolean; +}; export type SetupRemoteBucketResultType = typeof TYPE_CANCELLED | BucketSyncSetting; diff --git a/src/serviceFeatures/useReviewHarness.ts b/src/serviceFeatures/useReviewHarness.ts index 2f9c9791..cb56eb93 100644 --- a/src/serviceFeatures/useReviewHarness.ts +++ b/src/serviceFeatures/useReviewHarness.ts @@ -15,6 +15,8 @@ import { runReviewHarnessVaultRoundTrip, } from "@/features/ReviewHarness/reviewHarnessVaultFixture"; import type { CompatibilityReviewController } from "./compatibilityReview"; +import { runReviewHarnessIdBenchmark } from "@/features/ReviewHarness/reviewHarnessIdBenchmark"; +import { createIdBenchmarkOperations } from "@/features/ReviewHarness/reviewHarnessIdBenchmarkRuntime"; async function runVaultRoundTrip(plugin: ObsidianLiveSyncPlugin): Promise { const vault = plugin.app.vault; @@ -58,6 +60,8 @@ export function useReviewHarness( getCompatibilityPause: () => compatibilityReview.pendingPause, openCompatibilityReview: () => compatibilityReview.openReview(), runVaultRoundTrip: () => runVaultRoundTrip(plugin), + runIdBenchmark: async () => + runReviewHarnessIdBenchmark(await createIdBenchmarkOperations(), activeWindow.performance), readContinuation: () => services.setting.getSmallConfig(REVIEW_HARNESS_STATE_KEY), writeContinuation: (value) => services.setting.setSmallConfig(REVIEW_HARNESS_STATE_KEY, value), deleteContinuation: () => services.setting.deleteSmallConfig(REVIEW_HARNESS_STATE_KEY), diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index 39128c1b..e5aea396 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -129,6 +129,8 @@ The mobile pass uses Obsidian's `app.emulateMobile(true)`, a 390 by 844 CSS-pixe `test:e2e:obsidian:review-harness` exercises only the boundaries owned by the opt-in maintainer Harness. It retains a real compatibility pause, uses the fixed Harness restart action to persist a device-local continuation and reload Obsidian, and requires the Harness to delete that state before reopening. It also runs the bounded settings-lifecycle observation, confirms the dedicated Vault fixture root is removed, captures the copied privacy-bounded Markdown report, and checks the Harness layout and touch targets in mobile test mode. Compatibility explanation and persistence details remain owned by `settings-ui`, real P2P transfer remains owned by the dedicated P2P suites, and general Vault reflection remains owned by `vault-reflection`; the Harness test does not duplicate those workflows. +The Harness also measures ID generation with fixed in-memory data on desktop and mobile displays, checks that live settings remain unchanged, and verifies that the copied report includes both per-1,000-ID and per-ID timings, key derivation, and JavaScript heap availability. Mobile test mode verifies the UI and execution path; measure native device performance by running the same Harness on that device. + `test:e2e:obsidian:p2p-pane` starts one configured CouchDB-only session with no P2P profile and separate configured P2P sessions for desktop and mobile. It proves that the command remains registered while the retired command, automatic pane, and ribbon entry without a P2P configuration are absent. For the configured P2P profiles, it verifies that the desktop ribbon is available, the current status command reaches the pane without it opening at start-up, checks its connection control and horizontal layout, and captures unobstructed desktop and mobile screenshots. The mobile session uses a fresh Vault, profile, and Obsidian process, enters `app.emulateMobile(true)` through `lifecycle.beforePluginStart`, and requires the P2P view to belong to the right drawer rather than inheriting desktop workspace state. It deliberately uses no relay or peer: replacement of the active replicator is covered by focused unit tests, the Deno and Compose CLI P2P lifecycle suite covers the headless transport, and `p2p-setup-uri-workflow` owns the visible transfer path between two real Obsidian sessions. `test:e2e:obsidian:local-suite` builds the plug-in and, unless `LIVESYNC_CLI_COMMAND` selects an external CLI, the local LiveSync CLI. It then runs discovery, smoke, the onboarding invitation, Svelte dialogue mounting, revision repair, settings UI, the Review Harness, the P2P status pane, Vault reflection, CouchDB upload and manual setup, CLI-to-Obsidian synchronisation, Object Storage upload and Setup URI round-trip, P2P Setup URI round-trip, startup scan, provisioned CouchDB Setup URI, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export in sequence. Start the local CouchDB, RustFS, and P2P relay fixtures before running it, or use `test:e2e:obsidian:local-suite:services` to let the wrapper stop leftover fixtures, start fresh fixtures, and stop them again after the run. @@ -139,10 +141,16 @@ The same workflow checks the two remote-activity status boundaries. It first hol `test:e2e:obsidian:couchdb-manual-setup-workflow` follows the visible onboarding path for the first device when no Setup URI is available. It enters end-to-end encryption and CouchDB details, runs the read-only `Check server requirements` step, requires the prepared fixture to pass without applying a server fix, and lets the onboarding connection test create the named database. After Rebuild completes on the first device, it creates an ordinary note, asks that working device to generate a Setup URI for a second device, completes Fetch there, and verifies a bidirectional note round-trip. The workflow captures each decision point and the expanded server-check result; password controls remain visually masked. It uses an E2EE passphrase beginning with `%`, confirms that the saved settings do not contain it in plain text, and checks that Obsidian restores it after restarting with the first Vault. +The ordinary workflow now checks that all three ID-configuration radio choices are visible, disabled and dimmed while E2EE is off, and fully visible when it is enabled. It also checks that the random key is selected by default for a new Vault, **Keep current configuration** shows its legacy explanation, and the saved key is encrypted locally and transferred by Setup URI. A screenshot of the disabled group is saved as `guide-couchdb-manual-id-generation-disabled.png`. Set `E2E_OBSIDIAN_INDEPENDENT_IDS=true` for the same visible workflow with an explicitly entered, randomly generated source. That variant checks all three nested radio choices, requires a source when no key is saved, retains the saved key when a custom source is empty, rejects an ordinary string in the recovery-code input, restores the same key from a tagged code, verifies that the source is absent from local settings, and checks that both devices compute the same obfuscated document IDs after Setup URI import and Fast Fetch. + If this status workflow fails while Obsidian is running, it writes a full-page screenshot and a JSON snapshot of the status text and counters under `/tmp/obsidian-livesync-e2e`. The dialogue-mount workflow leaves desktop and mobile screenshots for both representative Svelte routes, the Hidden File Sync workflow captures the successfully displayed JSON Resolve dialogue before selecting an option, and the Security Seed reconnect workflow captures each significant application state. The suite therefore records representative evidence without capturing every interaction. Set `E2E_OBSIDIAN_DIAGNOSTICS_DIR` to use another directory. The two-Vault workflow performs the missing-marker review once for each isolated Vault. Later process launches reuse the same profile-backed acknowledgement, rather than seeding a replacement or repeatedly applying a decision for the first device. The Hidden File Sync scenario is narrower: it starts from an explicitly acknowledged marker because it tests consumer-owned hidden-file behaviour, JSON resolution, target filtering, and grouped mobile Notices rather than duplicating the compatibility workflow. After `app.emulateMobile(true)`, its fixture operations use the active DevTools renderer because Obsidian can remove desktop-only CLI commands in mobile mode. +The two-Vault workflow also covers independent ID derivation with two real Obsidian sessions: a note travels in each direction, both devices retain the same obfuscated document IDs, and identical content reuses the same Chunk IDs. Fresh devices with a different ID key or legacy ID configuration must be rejected by ordinary CouchDB replication before any remote document or checkpoint changes. Set `E2E_OBSIDIAN_ONLY_INDEPENDENT_IDS=true` to run that case without the other two-Vault scenarios. + +Set `E2E_OBSIDIAN_ONLY_DIFFERENT_CHUNK_ID_KEYS=true` to run the focused case where two devices use different saved ID keys with Path Obfuscation off. It verifies that each device can read the other's note, visible document IDs agree, and writing the same content produces different Chunk IDs. + `test:e2e:obsidian:cli-to-obsidian-sync` is the cross-runtime compatibility check for the official LiveSync CLI and the real Obsidian plug-in. Build the plug-in first, and build the local CLI too when no external CLI command is selected. The script uses E2EE, Path Obfuscation, and the current preferred chunk settings to create and synchronise a note through the CLI, starts real Obsidian with an isolated Vault and profile, synchronises the same CouchDB database, and verifies that the plug-in materialises identical note content. This covers the boundary that CLI-only and plug-in-only round trips do not exercise. The isolated Obsidian session starts with its CouchDB settings and device-local compatibility acknowledgement already in place. This keeps the scenario focused on cross-runtime data compatibility; unconfigured start-up and visible CouchDB onboarding are covered by their dedicated workflows. @@ -166,6 +174,9 @@ LIVESYNC_CLI_COMMAND="docker run --rm --network host --user $(id -u):$(id -g) -- `test:e2e:obsidian:minio-upload` reuses the Object Storage variables from `.test.env` or the process environment. It expects a reachable S3-compatible service and starts with isolated Object Storage settings and the device-local compatibility acknowledgement already in place, keeping the scenario focused on upload rather than unconfigured start-up or setup. It confirms those settings through `obsidian-cli eval`, creates a note in real Obsidian, runs one-shot Journal Sync, and verifies through the AWS SDK that objects were written under a unique bucket prefix. Adapter tests separately observe an in-progress SDK command, while this real-runtime workflow verifies the resulting request counters advance and rebalance. +Set `E2E_OBSIDIAN_INDEPENDENT_IDS=true` to run the same upload with E2EE, Path Obfuscation, and a separately derived ID key. The scenario verifies the local document and Chunk ID shapes before the Journal transfer. +Set `E2E_OBSIDIAN_CUSTOM_HTTP_HANDLER=true` when the local Object Storage fixture does not allow browser requests from Obsidian's renderer. + `test:e2e:obsidian:object-storage-setup-uri-workflow` uses the public Commonlib-backed tool to generate the initial Setup URI for a unique Object Storage prefix, completes visible initialisation on the first device, and then asks that working real Obsidian device to create a new Setup URI through the registered command. A second real Obsidian device imports only the device-generated URI. The workflow verifies the A-to-B note through explicit replication, then verifies that the B-to-A note arrives through `syncOnStart` after restarting the first device, without requesting manual replication. It captures the documented onboarding choices, and removes the Object Storage prefix only after both sessions have stopped. `test:e2e:obsidian:p2p-setup-uri-workflow` runs two concurrent isolated real Obsidian sessions against the local Compose Nostr relay fixture. The first device imports a generated initial Setup URI and completes its signalling test with zero peers, creates a Setup URI for the second device through the registered command, and remains online while the second device imports it. The second device must select the expected online source before Fetch can rebuild its local database. The workflow accepts each connection request visibly on the receiving device, verifies the initial A-to-B fetch, checks that the menu for the three persistent per-peer actions remains within the viewport, reconnects both P2P sessions in join order, and verifies the B-to-A return journey. Every started session remains tracked until teardown completes. @@ -204,7 +215,7 @@ This proves in real Obsidian the plug-in behaviour shared by supported platforms `test:e2e:obsidian:internal-metadata-migration` enables internal Metadata encryption through the settings UI without Rebuild. It checks unchanged plaintext and rewritten encrypted Hidden File Sync and Customisation Sync Metadata in CouchDB, stable document IDs, mismatch rejection on a second device, and file restoration after aligning settings. It then turns the preference OFF, runs Fast Fetch, and compares content loaded from both Metadata representations and their Chunks while retaining the remote feature declaration. These focused tests use the local CouchDB fixture and are outside `test:e2e:obsidian:local-suite`. -`test:e2e:obsidian:setting-markdown-export` enables setting Markdown export, waits for the generated Markdown file in the vault, and verifies that credentials are omitted when `writeCredentialsForSettingSync=false`. +`test:e2e:obsidian:setting-markdown-export` enables setting Markdown export, waits for the generated Markdown file in the vault, and verifies that credentials are omitted when `writeCredentialsForSettingSync=false`, including both the plaintext ID key and its encrypted local representation. `test:e2e:obsidian:upgrade-from-stable` is the release-acceptance upgrade workflow. It installs the exact published 0.25.83 artefacts into an isolated Vault, verifies their pinned SHA-256 values, and then replaces only the plug-in artefacts with the current target while retaining the same Vault and isolated Obsidian profile. The first run downloads the old release into the ignored `_testdata/releases` cache; every later run verifies the cached bytes before use. diff --git a/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts b/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts index 4e1945b4..b4468ffc 100644 --- a/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts +++ b/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts @@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { DEVICE_ID_PREFERRED, MILESTONE_DOCID } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { deriveIdKey, formatIdRecoveryCode } from "@vrtmrz/livesync-commonlib/settings"; import { evalObsidianJson } from "../runner/cli.ts"; import { assertCouchDbReachable, @@ -99,7 +100,12 @@ async function captureFailure(session: ObsidianLiveSyncSession, label: string): } } -async function enterManualCouchDBSettings(port: number, couchDb: CouchDbConfig, dbName: string): Promise { +async function enterManualCouchDBSettings( + port: number, + couchDb: CouchDbConfig, + dbName: string, + independentIdSource?: string +): Promise { const screenshots: string[] = []; await withObsidianPage(port, async (page) => { const invitation = page.locator(".notice").filter({ hasText: "Welcome to Self-hosted LiveSync" }); @@ -134,12 +140,41 @@ async function enterManualCouchDBSettings(port: number, couchDb: CouchDbConfig, 0, "The Obfuscate Properties row was present before end-to-end encryption was enabled." ); + const disabledIdChoices = encryption.locator("fieldset.sls-id-choices").first(); + assertEqual( + await disabledIdChoices.evaluate((element) => element.hasAttribute("disabled")), + true, + "The ID configuration was enabled while E2EE was off." + ); + for (const value of ["keep", "random", "custom"]) { + assertEqual( + await disabledIdChoices.locator(`input[value="${value}"]`).isDisabled(), + true, + `The ${value} ID configuration was enabled while E2EE was off.` + ); + } + const disabledIdScreenshot = join( + process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e", + "guide-couchdb-manual-id-generation-disabled.png" + ); + await disabledIdChoices.screenshot({ path: disabledIdScreenshot }); + screenshots.push(disabledIdScreenshot); + assertEqual( + await disabledIdChoices.evaluate((element) => Number(getComputedStyle(element).opacity) < 1), + true, + "The disabled ID configuration did not look disabled in the default theme." + ); await encryption .locator("label.row") .filter({ hasText: "End-to-End Encryption" }) .locator('input[type="checkbox"]') .first() .check({ timeout: uiTimeoutMs }); + assertEqual( + await disabledIdChoices.evaluate((element) => Number(getComputedStyle(element).opacity)), + 1, + "The ID configuration remained dimmed after E2EE was enabled." + ); const passphraseInput = encryption.locator('input[name="e2ee-passphrase"]'); await passphraseInput.waitFor({ state: "visible", timeout: uiTimeoutMs }); await encryption @@ -149,7 +184,85 @@ async function enterManualCouchDBSettings(port: number, couchDb: CouchDbConfig, .first() .check({ timeout: uiTimeoutMs }); await passphraseInput.fill(e2eePassphrase); - const passwordToggle = encryption.locator("button.sls-password-toggle"); + const idChoices = encryption.locator('input[type="radio"][name="id-derivation-choice"]'); + assertEqual(await idChoices.count(), 3, "The three ID configurations were not all shown."); + for (const value of ["keep", "random", "custom"]) { + assertEqual( + await encryption.locator(`input[name="id-derivation-choice"][value="${value}"]`).isVisible(), + true, + `The ${value} ID configuration was not visible.` + ); + } + const keepChoice = encryption.locator('input[name="id-derivation-choice"][value="keep"]'); + const randomChoice = encryption.locator('input[name="id-derivation-choice"][value="random"]'); + const customChoice = encryption.locator('input[name="id-derivation-choice"][value="custom"]'); + assertEqual(await randomChoice.isChecked(), true, "The default ID configuration was not random."); + assertEqual( + await encryption.getByText("Keep current configuration", { exact: true }).count(), + 1, + "The current-configuration choice was not labelled consistently." + ); + assertEqual( + await encryption.getByText("Current configuration: no ID key is saved.", { exact: false }).count(), + 1, + "The current legacy configuration was not explained." + ); + await keepChoice.check({ timeout: uiTimeoutMs }); + assertEqual( + await encryption.getByText("Changing the E2EE passphrase changes IDs", { exact: false }).count(), + 1, + "Keeping legacy IDs did not explain the effect of changing the E2EE passphrase." + ); + await randomChoice.check({ timeout: uiTimeoutMs }); + if (independentIdSource) { + await customChoice.check({ timeout: uiTimeoutMs }); + const customChoices = encryption.locator('input[type="radio"][name="id-custom-choice"]'); + assertEqual(await customChoices.count(), 3, "The three custom ID inputs were not all shown."); + for (const value of ["passphrase", "source", "recovery"]) { + assertEqual( + await encryption.locator(`input[name="id-custom-choice"][value="${value}"]`).isVisible(), + true, + `The ${value} custom ID input was not visible.` + ); + } + const sourceChoice = encryption.locator('input[name="id-custom-choice"][value="source"]'); + assertEqual(await sourceChoice.isChecked(), true, "The custom ID input was not selected by default."); + await encryption + .locator('input[name="id-custom-choice"][value="passphrase"]') + .check({ timeout: uiTimeoutMs }); + assertEqual( + await encryption.locator('input[name="id-derivation-source"]').count(), + 0, + "The E2EE passphrase choice exposed a second source input." + ); + await encryption + .locator('input[name="id-custom-choice"][value="recovery"]') + .check({ timeout: uiTimeoutMs }); + assertEqual( + await encryption.locator('input[name="id-derivation-source"]').getAttribute("placeholder"), + "Enter an ID recovery code", + "The recovery-code choice did not request a recovery code." + ); + const recoveryChoice = encryption.locator('input[name="id-custom-choice"][value="recovery"]'); + await sourceChoice.check({ timeout: uiTimeoutMs }); + const sourceInput = encryption.locator('input[name="id-derivation-source"]'); + await sourceInput.fill(""); + await encryption.getByRole("button", { name: "Proceed", exact: true }).click({ timeout: uiTimeoutMs }); + assertEqual( + await encryption.getByText("An ID source is required to enable this option.", { exact: false }).count(), + 1, + "A first-time independent ID configuration did not require a source." + ); + assertEqual( + await encryption.isVisible(), + true, + "The E2EE dialogue closed after a first-time ID source was omitted." + ); + await recoveryChoice.check({ timeout: uiTimeoutMs }); + await sourceChoice.check({ timeout: uiTimeoutMs }); + await sourceInput.fill(independentIdSource); + } + const passwordToggle = passphraseInput.locator("..").locator("button.sls-password-toggle"); await passwordToggle.click({ timeout: uiTimeoutMs }); assertEqual( await passphraseInput.getAttribute("type"), @@ -167,16 +280,13 @@ async function enterManualCouchDBSettings(port: number, couchDb: CouchDbConfig, "password", "Toggling visibility again did not re-mask the passphrase." ); - assertEqual( - await passphraseInput.inputValue(), - e2eePassphrase, - "Re-masking the passphrase changed its value." - ); + assertEqual(await passphraseInput.inputValue(), e2eePassphrase, "Re-masking the passphrase changed its value."); }); screenshots.push(await captureGuideDialogue(port, "guide-couchdb-manual-encryption.png", "End-to-End Encryption")); await withObsidianPage(port, async (page) => { const encryption = modalByTitle(page, "End-to-End Encryption"); await encryption.getByRole("button", { name: "Proceed", exact: true }).click({ timeout: uiTimeoutMs }); + await encryption.waitFor({ state: "hidden", timeout: uiTimeoutMs }); }); screenshots.push( @@ -288,13 +398,18 @@ async function waitForRemoteEntry(context: RunnerContext, entry: { id: string; c }); } -async function assertPersistedE2EE(vault: TemporaryVault): Promise { - const persisted = JSON.parse( - await readFile(join(vault.path, ".obsidian", "plugins", "obsidian-livesync", "data.json"), "utf8") - ) as { +async function assertPersistedE2EE(vault: TemporaryVault, independentIdSource?: string): Promise { + const rawSettings = await readFile( + join(vault.path, ".obsidian", "plugins", "obsidian-livesync", "data.json"), + "utf8" + ); + const persisted = JSON.parse(rawSettings) as { encrypt?: unknown; encryptedPassphrase?: unknown; passphrase?: unknown; + idDerivationVersion?: unknown; + idDerivationKey?: unknown; + encryptedIdDerivationKey?: unknown; }; assertEqual(persisted.encrypt, true, "Manual CouchDB setup did not persist E2EE as enabled."); assertEqual(persisted.passphrase, "", "Manual CouchDB setup persisted the E2EE passphrase in plain text."); @@ -304,6 +419,14 @@ async function assertPersistedE2EE(vault: TemporaryVault): Promise { if (JSON.stringify(persisted).includes(e2eePassphrase)) { throw new Error("Manual CouchDB setup persisted the E2EE passphrase in plain text."); } + assertEqual(persisted.idDerivationVersion, 1, "The independent ID mode was not persisted."); + assertEqual(persisted.idDerivationKey, "", "The derived ID key was stored in plain text."); + if (typeof persisted.encryptedIdDerivationKey !== "string" || !persisted.encryptedIdDerivationKey) { + throw new Error("The derived ID key was not encrypted in local settings."); + } + if (independentIdSource) { + if (rawSettings.includes(independentIdSource)) throw new Error("The ID source was stored in local settings."); + } } async function assertRestoredE2EEPassphrase(session: ObsidianLiveSyncSession, cliBinary: string): Promise { @@ -320,6 +443,126 @@ async function assertRestoredE2EEPassphrase(session: ObsidianLiveSyncSession, cl assertEqual(restored, true, "The E2EE passphrase was not restored after Obsidian restarted."); } +async function assertCurrentIdDerivationKey( + session: ObsidianLiveSyncSession, + cliBinary: string, + expected: string, + context: string +): Promise { + const settings = await evalObsidianJson<{ idDerivationVersion: number; idDerivationKey: string }>( + cliBinary, + [ + "(()=>{", + "const settings=app.plugins.plugins['obsidian-livesync'].core.services.setting.currentSettings();", + "return JSON.stringify({idDerivationVersion:settings.idDerivationVersion,idDerivationKey:settings.idDerivationKey});", + "})()", + ].join(""), + session.cliEnv + ); + assertEqual(settings.idDerivationVersion, 1, `${context}: the independent ID version was not retained.`); + assertEqual(settings.idDerivationKey, expected, `${context}: the saved ID key changed.`); +} + +async function assertRecoveryCodeCanBeRevealed( + session: ObsidianLiveSyncSession, + cliBinary: string, + source: string +): Promise { + const port = session.remoteDebuggingPort; + const expected = formatIdRecoveryCode(await deriveIdKey(source)); + await withObsidianPage(port, async (page) => { + const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs); + const remotePage = await settingsNavigator.openPage("Remote Configuration"); + await remotePage + .locator(".setting-item") + .filter({ hasText: "Configure E2EE" }) + .getByRole("button", { name: "Configure", exact: true }) + .click({ timeout: uiTimeoutMs }); + const encryption = modalByTitle(page, "End-to-End Encryption"); + await encryption.waitFor({ state: "visible", timeout: uiTimeoutMs }); + assertEqual( + await encryption.locator('input[name="id-derivation-choice"][value="keep"]').isChecked(), + true, + "An existing ID key was not selected for reuse." + ); + assertEqual( + await encryption.getByText("Current configuration: a saved ID key is used.").count(), + 1, + "The saved ID key was not explained." + ); + await encryption.getByRole("button", { name: "Show current recovery code" }).click({ timeout: uiTimeoutMs }); + assertEqual( + await encryption.getByRole("textbox", { name: "Current ID recovery code" }).inputValue(), + expected, + "The displayed recovery code did not contain the saved ID key." + ); + await encryption.locator('input[name="id-derivation-choice"][value="custom"]').check({ timeout: uiTimeoutMs }); + await encryption.locator('input[name="id-custom-choice"][value="recovery"]').check({ timeout: uiTimeoutMs }); + const recoveryInput = encryption.locator('input[name="id-derivation-source"]'); + await recoveryInput.fill("not-a-recovery-code"); + await encryption.getByRole("button", { name: "Proceed" }).click({ timeout: uiTimeoutMs }); + assertEqual( + await encryption.getByText("The ID source or recovery code is invalid.", { exact: false }).count(), + 1, + "The recovery-code choice accepted an ordinary source string." + ); + await recoveryInput.fill(expected); + await encryption.getByRole("button", { name: "Proceed" }).click({ timeout: uiTimeoutMs }); + await encryption.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + assertEqual( + await modalByTitle(page, "Mostly Complete: Decision Required").count(), + 0, + "Recovering the existing ID key opened a new setup decision." + ); + }); + const expectedIdKey = await deriveIdKey(source); + await assertCurrentIdDerivationKey(session, cliBinary, expectedIdKey, "Recovering the saved ID key"); + + await withObsidianPage(port, async (page) => { + const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs); + const remotePage = await settingsNavigator.openPage("Remote Configuration"); + await remotePage + .locator(".setting-item") + .filter({ hasText: "Configure E2EE" }) + .getByRole("button", { name: "Configure", exact: true }) + .click({ timeout: uiTimeoutMs }); + const encryption = modalByTitle(page, "End-to-End Encryption"); + await encryption.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await encryption.locator('input[name="id-derivation-choice"][value="custom"]').check({ timeout: uiTimeoutMs }); + await encryption.locator('input[name="id-custom-choice"][value="source"]').check({ timeout: uiTimeoutMs }); + const sourceInput = encryption.locator('input[name="id-derivation-source"]'); + await sourceInput.fill(""); + assertEqual(await sourceInput.inputValue(), "", "The independent ID source input was not empty."); + assertEqual( + await encryption.getByText("Leave this input empty to keep the saved ID key.", { exact: true }).count(), + 1, + "The configured ID source did not explain that an empty input keeps the saved ID key." + ); + await encryption.getByRole("button", { name: "Proceed", exact: true }).click({ timeout: uiTimeoutMs }); + await encryption.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + assertEqual( + await modalByTitle(page, "Mostly Complete: Decision Required").count(), + 0, + "Keeping the saved ID key after an empty source opened a new setup decision." + ); + }); + await assertCurrentIdDerivationKey(session, cliBinary, expectedIdKey, "Saving an empty custom ID source"); +} + +async function readDocumentId(cliBinary: string, environment: NodeJS.ProcessEnv, path: string): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "return JSON.stringify(await core.services.path.path2id(path));", + "})()", + ].join(""), + environment + ); +} + async function setRemotePreferredE2EEDisabled(context: RunnerContext): Promise { const milestone = await fetchCouchDbDocument(context.couchDb, context.dbName, MILESTONE_DOCID); const tweakValues = milestone.tweak_values; @@ -430,6 +673,10 @@ async function main(): Promise { }; const screenshots: string[] = []; let secondDeviceArtifact: SetupArtifact | undefined; + const independentIdSource = + process.env.E2E_OBSIDIAN_INDEPENDENT_IDS === "true" ? randomBytes(32).toString("base64url") : undefined; + let firstEntryId: string | undefined; + let returnEntryId: string | undefined; try { await assertCouchDbReachable(couchDb); @@ -440,7 +687,9 @@ async function main(): Promise { let session = await startUnconfiguredSession(context, vaultA); try { - screenshots.push(...(await enterManualCouchDBSettings(session.remoteDebuggingPort, couchDb, dbName))); + screenshots.push( + ...(await enterManualCouchDBSettings(session.remoteDebuggingPort, couchDb, dbName, independentIdSource)) + ); screenshots.push(await captureAndStartInitialisation(session.remoteDebuggingPort, "new", captures)); screenshots.push(await confirmRebuild(session.remoteDebuggingPort, captures)); screenshots.push(await continueWithoutRemoteSettings(session.remoteDebuggingPort, captures)); @@ -453,10 +702,14 @@ async function main(): Promise { 1, "Manual CouchDB setup did not persist exactly one remote profile." ); - await assertPersistedE2EE(vaultA); + await assertPersistedE2EE(vaultA, independentIdSource); + if (independentIdSource) { + await assertRecoveryCodeCanBeRevealed(session, context.cliBinary, independentIdSource); + } await writeNoteViaObsidian(context.cliBinary, session.cliEnv, notePath, noteContent); const entry = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, notePath); + firstEntryId = entry.id; await pushLocalChanges(context.cliBinary, session.cliEnv); await waitForRemoteEntry(context, entry); } catch (error) { @@ -479,9 +732,12 @@ async function main(): Promise { ); await finishInitialisation(session.remoteDebuggingPort, context.cliBinary, session.cliEnv); await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort); - await assertPersistedE2EE(vaultA); + await assertPersistedE2EE(vaultA, independentIdSource); const rebuiltEntry = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, notePath); + if (independentIdSource) { + assertEqual(rebuiltEntry.id, firstEntryId, "Rebuild changed the configured document ID."); + } await waitForRemoteEntry(context, rebuiltEntry); await assertRemoteEntryEncrypted(context, rebuiltEntry, notePath, noteContent); await assertRemotePreferredE2EE(context, true); @@ -512,11 +768,20 @@ async function main(): Promise { screenshots.push(...(await confirmFastFetch(session.remoteDebuggingPort, captures))); await finishInitialisation(session.remoteDebuggingPort, context.cliBinary, session.cliEnv); await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort); + await assertPersistedE2EE(vaultB, independentIdSource); await pushLocalChanges(context.cliBinary, session.cliEnv); await waitForVaultFile(vaultB, notePath, noteContent); + if (independentIdSource) { + assertEqual( + await readDocumentId(context.cliBinary, session.cliEnv, notePath), + firstEntryId, + "The Setup URI did not restore the document ID key on the second device." + ); + } await writeNoteViaObsidian(context.cliBinary, session.cliEnv, returnNotePath, returnNoteContent); const returnEntry = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, returnNotePath); + returnEntryId = returnEntry.id; await pushLocalChanges(context.cliBinary, session.cliEnv); await waitForRemoteEntry(context, returnEntry); } catch (error) { @@ -531,6 +796,13 @@ async function main(): Promise { await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort); await pushLocalChanges(context.cliBinary, session.cliEnv); await waitForVaultFile(vaultA, returnNotePath, returnNoteContent); + if (independentIdSource) { + assertEqual( + await readDocumentId(context.cliBinary, session.cliEnv, returnNotePath), + returnEntryId, + "The first device did not retain the shared document ID key." + ); + } } catch (error) { await captureFailure(session, "return-journey"); throw error; diff --git a/test/e2e-obsidian/scripts/minio-upload.ts b/test/e2e-obsidian/scripts/minio-upload.ts index b0899f12..7de062e8 100644 --- a/test/e2e-obsidian/scripts/minio-upload.ts +++ b/test/e2e-obsidian/scripts/minio-upload.ts @@ -15,6 +15,8 @@ * Separate successes would not prove that those observations belonged to the * same upload. */ +import { randomBytes } from "node:crypto"; +import { deriveIdKey } from "@vrtmrz/livesync-commonlib/settings"; import { evalObsidianJson } from "../runner/cli.ts"; import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; import { @@ -33,6 +35,7 @@ import { listObjectStorageObjects, loadObjectStorageConfig, makeUniqueBucketPrefix, + readObjectStorageJson, } from "../runner/objectStorage.ts"; import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; import { createTemporaryVault } from "../runner/vault.ts"; @@ -41,6 +44,8 @@ import { REMOTE_ACTIVITY_EXPECTED_STATE, waitForRemoteActivityState } from "../r process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "30000"; const notePath = "E2E/minio-upload.md"; +const useIndependentIds = process.env.E2E_OBSIDIAN_INDEPENDENT_IDS === "true"; +const useCustomRequestHandler = process.env.E2E_OBSIDIAN_CUSTOM_HTTP_HANDLER === "true"; const noteContent = [ "# Object Storage upload from real Obsidian", "", @@ -127,10 +132,23 @@ async function main(): Promise { }); await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); - const configured = await configureObjectStorage(cli.binary, session.cliEnv, { - ...objectStorage, - bucketPrefix, - }); + const configured = await configureObjectStorage( + cli.binary, + session.cliEnv, + { ...objectStorage, bucketPrefix }, + { + ...(useIndependentIds + ? { + encrypt: true, + usePathObfuscation: true, + passphrase: randomBytes(32).toString("base64url"), + idDerivationVersion: 1, + idDerivationKey: await deriveIdKey(randomBytes(32).toString("base64url")), + } + : {}), + ...(useCustomRequestHandler ? { useCustomRequestHandler: true } : {}), + } + ); await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); assertEqual(configured.isConfigured, true, "Self-hosted LiveSync was not marked as configured."); assertEqual(configured.remoteType, "MINIO", "Remote type was not Object Storage."); @@ -145,6 +163,16 @@ async function main(): Promise { REMOTE_ACTIVITY_EXPECTED_STATE.idle ); const localEntry = await createNoteAndWaitForLocalDb(cli.binary, session.cliEnv); + if (useIndependentIds) { + if ( + !/^f:[0-9a-f]{64}$/u.test(localEntry.id) || + localEntry.children.some((child) => !/^h:\+[0-9a-f]{64}$/u.test(child)) + ) { + throw new Error( + `The real Obsidian Journal upload did not use independent document and Chunk IDs (document length ${localEntry.id.length}, Chunk lengths ${localEntry.children.map((child) => child.length).join(",")}).` + ); + } + } await pushLocalChanges(cli.binary, session.cliEnv); const activityAfterUpload = await waitForRemoteActivityState( session.remoteDebuggingPort, @@ -160,6 +188,15 @@ async function main(): Promise { ); const keys = await waitForObjectStorageObjects(bucketPrefix); + if (useIndependentIds) { + const milestone = await readObjectStorageJson<{ encrypted_id_derivation_proof?: string }>( + objectStorage, + `${bucketPrefix}_00000000-milestone.json` + ); + if (!milestone.encrypted_id_derivation_proof) { + throw new Error("The Journal milestone did not retain an encrypted ID agreement proof."); + } + } console.log( `Uploaded ${localEntry.path} through Journal Sync to ${objectStorage.bucket}/${bucketPrefix} (${keys.length} object(s)); tracked requests: ${activityAfterUpload.requestCount - activityBeforeUpload.requestCount}` diff --git a/test/e2e-obsidian/scripts/review-harness.ts b/test/e2e-obsidian/scripts/review-harness.ts index 383074c7..853b1233 100644 --- a/test/e2e-obsidian/scripts/review-harness.ts +++ b/test/e2e-obsidian/scripts/review-harness.ts @@ -6,6 +6,7 @@ import { assertNoHorizontalOverflow, } from "@vrtmrz/obsidian-test-session"; import { CURRENT_SETTING_VERSION } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const"; +import { DoctorRegulation } from "@vrtmrz/livesync-commonlib/compat/common/configForDoc"; import { REVIEW_HARNESS_STATE_KEY } from "../../../src/features/ReviewHarness/reviewHarnessController.ts"; import { REVIEW_HARNESS_FIXTURE_ROOT } from "../../../src/features/ReviewHarness/reviewHarnessVaultFixture.ts"; import { evalObsidianJson } from "../runner/cli.ts"; @@ -166,7 +167,8 @@ async function captureReadinessFailure( async function openHarness(): Promise { const opened = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { return await page.evaluate( - (commandId) => (globalThis as ReviewHarnessTestGlobal).app?.commands?.executeCommandById(commandId) === true, + (commandId) => + (globalThis as ReviewHarnessTestGlobal).app?.commands?.executeCommandById(commandId) === true, "obsidian-livesync:open-review-harness" ); }); @@ -205,11 +207,54 @@ async function runAutomaticScenarios(): Promise { }); } +async function runIdBenchmark(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const snapshotSettings = () => + page.evaluate(() => { + const plugin = (globalThis as ReviewHarnessTestGlobal).app?.plugins?.plugins["obsidian-livesync"] as { + core: { services: { setting: { currentSettings(): unknown } } }; + }; + return JSON.stringify(plugin.core.services.setting.currentSettings()); + }); + const before = await snapshotSettings(); + const harness = page.locator('[data-testid="review-harness"]'); + await harness + .locator('[data-testid="review-harness-run-id-generation-performance"]') + .click({ timeout: uiTimeoutMs }); + const result = harness.locator('[data-testid="review-harness-result-id-generation-performance"]'); + await result.getByText("Passed:", { exact: false }).waitFor({ state: "visible", timeout: uiTimeoutMs * 4 }); + const observations = await result.locator("li").allTextContents(); + for (const label of [ + "Chunk IDs, 256 B", + "Chunk IDs, 4096 B", + "Chunk IDs, 32768 B", + "Obfuscated document IDs", + ]) { + for (const mode of ["legacy", "independent"]) { + if ( + !observations.some( + (line) => + line.startsWith(`${label}, ${mode}: 1000 IDs total median=`) && line.includes("; per ID=") + ) + ) { + throw new Error(`Missing benchmark timing and units: ${label}, ${mode}`); + } + } + } + if ( + !observations.some((line) => line.startsWith("ID key derivation at save time:")) || + !observations.some((line) => line.startsWith("JavaScript heap:")) + ) { + throw new Error("The benchmark did not report derivation and heap observations."); + } + if ((await snapshotSettings()) !== before) throw new Error("The benchmark changed the live settings."); + await assertNoHorizontalOverflow(page, harness, { label: "ID benchmark results" }); + }); +} + async function runVaultFixture(): Promise { await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { - await page - .locator('[data-testid="review-harness-run-vault-round-trip"]') - .click({ timeout: uiTimeoutMs }); + await page.locator('[data-testid="review-harness-run-vault-round-trip"]').click({ timeout: uiTimeoutMs }); const confirmation = page.locator(".modal-container").filter({ has: page.getByText("Review Harness: Vault fixture access", { exact: true }), }); @@ -272,27 +317,22 @@ async function restartAndResumeHarness(): Promise { }); await keepCompatibilityPaused(); await waitForHarness(); - return await captureObsidianDialogue( - obsidianRemoteDebuggingPort(), - "review-harness-resumed.png", - async (page) => { - const harness = page.locator('[data-testid="review-harness"]'); - await harness - .locator('[data-testid="review-harness-resumed"]') - .waitFor({ state: "visible", timeout: uiTimeoutMs }); - const continuationRemoved = await page.evaluate((stateKey) => { - const plugin = (globalThis as ReviewHarnessTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; - if (typeof plugin !== "object" || plugin === null || !("core" in plugin)) { - throw new Error("Self-hosted LiveSync is unavailable after restart."); - } - const core = (plugin as { core: { services: { setting: { getSmallConfig(key: string): string } } } }) - .core; - return core.services.setting.getSmallConfig(stateKey) === ""; - }, REVIEW_HARNESS_STATE_KEY); - if (!continuationRemoved) throw new Error("The one-shot continuation was not removed before use."); - await assertNoHorizontalOverflow(page, harness, { label: "resumed Review Harness" }); - } - ); + return await captureObsidianDialogue(obsidianRemoteDebuggingPort(), "review-harness-resumed.png", async (page) => { + const harness = page.locator('[data-testid="review-harness"]'); + await harness + .locator('[data-testid="review-harness-resumed"]') + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + const continuationRemoved = await page.evaluate((stateKey) => { + const plugin = (globalThis as ReviewHarnessTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (typeof plugin !== "object" || plugin === null || !("core" in plugin)) { + throw new Error("Self-hosted LiveSync is unavailable after restart."); + } + const core = (plugin as { core: { services: { setting: { getSmallConfig(key: string): string } } } }).core; + return core.services.setting.getSmallConfig(stateKey) === ""; + }, REVIEW_HARNESS_STATE_KEY); + if (!continuationRemoved) throw new Error("The one-shot continuation was not removed before use."); + await assertNoHorizontalOverflow(page, harness, { label: "resumed Review Harness" }); + }); } async function completeResumedCompatibilityStep(): Promise { @@ -331,9 +371,7 @@ async function copyAndReadReport(): Promise { undefined, { timeout: uiTimeoutMs } ); - return await page.evaluate( - () => (globalThis as ReviewHarnessTestGlobal).reviewHarnessCopiedReport ?? "" - ); + return await page.evaluate(() => (globalThis as ReviewHarnessTestGlobal).reviewHarnessCopiedReport ?? ""); }); } @@ -347,35 +385,36 @@ async function verifyMobileHarness(): Promise { if (typeof plugin !== "object" || plugin === null || !("core" in plugin)) { throw new Error("Self-hosted LiveSync is unavailable in mobile test mode."); } - const core = (plugin as { - core: { services: { API: { showWindow(type: string): Promise } } }; - }).core; + const core = ( + plugin as { + core: { services: { API: { showWindow(type: string): Promise } } }; + } + ).core; await core.services.API.showWindow(viewType); }, "self-hosted-livesync-review-harness"); }); - return await captureObsidianDialogue( - obsidianRemoteDebuggingPort(), - "review-harness-mobile.png", - async (page) => { - const harness = page.locator('[data-testid="review-harness"]'); - await harness.waitFor({ state: "visible", timeout: uiTimeoutMs }); - await assertNoHorizontalOverflow(page, harness, { label: "mobile Review Harness" }); - const heading = harness.getByRole("heading", { name: "Self-hosted LiveSync review harness" }); - await assertLocatorWithinSafeArea(page, heading, { - label: "mobile Review Harness heading", - safeAreaInsets: iPhoneSafeArea, + await runIdBenchmark(); + return await captureObsidianDialogue(obsidianRemoteDebuggingPort(), "review-harness-mobile.png", async (page) => { + const harness = page.locator('[data-testid="review-harness"]'); + await harness.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await harness.getByRole("heading", { name: "Self-hosted LiveSync review harness" }).scrollIntoViewIfNeeded(); + await assertNoHorizontalOverflow(page, harness, { label: "mobile Review Harness" }); + const heading = harness.getByRole("heading", { name: "Self-hosted LiveSync review harness" }); + await assertLocatorWithinSafeArea(page, heading, { + label: "mobile Review Harness heading", + safeAreaInsets: iPhoneSafeArea, + }); + for (const testId of [ + "review-harness-run-automatic", + "review-harness-run-full", + "review-harness-copy-report", + "review-harness-run-id-generation-performance", + ]) { + await assertLocatorHasMinimumTouchTarget(page, harness.locator(`[data-testid="${testId}"]`), { + label: testId, }); - for (const testId of [ - "review-harness-run-automatic", - "review-harness-run-full", - "review-harness-copy-report", - ]) { - await assertLocatorHasMinimumTouchTarget(page, harness.locator(`[data-testid="${testId}"]`), { - label: testId, - }); - } } - ); + }); } async function main(): Promise { @@ -391,7 +430,8 @@ async function main(): Promise { vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), pluginData: { - doctorProcessedVersion: "1.0.0", + // Config Doctor is covered by settings-ui; this fixture exercises the Harness. + doctorProcessedVersion: DoctorRegulation.version, settingVersion: CURRENT_SETTING_VERSION, isConfigured: true, additionalSuffixOfDatabaseName: "", @@ -440,15 +480,34 @@ async function main(): Promise { const vaultConfirmationScreenshot = await runVaultFixture(); const resumedScreenshot = await restartAndResumeHarness(); await completeResumedCompatibilityStep(); + await runIdBenchmark(); const report = await copyAndReadReport(); if (!report.includes("## Self-hosted LiveSync Review Harness report")) { throw new Error("The copied Review Harness report was not Markdown evidence."); } - for (const forbidden of [vault.name, REVIEW_HARNESS_FIXTURE_ROOT]) { - if (report.includes(forbidden)) throw new Error(`The Review Harness report exposed local state: ${forbidden}`); + for (const expected of [ + "1000 IDs total median=", + "; per ID=", + "ID key derivation at save time:", + "JavaScript heap:", + ]) { + if (!report.includes(expected)) throw new Error(`Missing copied benchmark observation: ${expected}`); + } + for (const forbidden of [ + vault.name, + REVIEW_HARNESS_FIXTURE_ROOT, + "ab".repeat(32), + "Self-hosted LiveSync ID benchmark passphrase", + "Self-hosted LiveSync ID benchmark source", + ]) { + if (report.includes(forbidden)) + throw new Error(`The Review Harness report exposed local state: ${forbidden}`); } const mobileScreenshot = await verifyMobileHarness(); + const outputDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e"; + await mkdir(outputDirectory, { recursive: true }); + await writeFile(join(outputDirectory, "review-harness-report.md"), report, "utf8"); console.log( `Review Harness passed one-shot, fixture, report, and mobile checks. Screenshots: ${[ initialScreenshot, diff --git a/test/e2e-obsidian/scripts/setting-markdown-export.ts b/test/e2e-obsidian/scripts/setting-markdown-export.ts index 44a5825e..d4a24291 100644 --- a/test/e2e-obsidian/scripts/setting-markdown-export.ts +++ b/test/e2e-obsidian/scripts/setting-markdown-export.ts @@ -1,5 +1,6 @@ import { readFile } from "node:fs/promises"; import { join } from "node:path"; +import { deriveIdKey } from "@vrtmrz/livesync-commonlib/settings"; import { evalObsidianJson } from "../runner/cli.ts"; import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; import { assertEqual } from "../runner/liveSyncWorkflow.ts"; @@ -34,7 +35,11 @@ async function waitForFileContaining( throw new Error(`Timed out waiting for setting Markdown: ${fullPath}\nLast error: ${String(lastError)}`); } -async function configureSettingMarkdown(cliBinary: string, env: NodeJS.ProcessEnv): Promise { +async function configureSettingMarkdown( + cliBinary: string, + env: NodeJS.ProcessEnv, + idDerivationKey: string +): Promise { await evalObsidianJson( cliBinary, [ @@ -46,6 +51,8 @@ async function configureSettingMarkdown(cliBinary: string, env: NodeJS.ProcessEn "couchDB_USER:'e2e-user',", "couchDB_PASSWORD:'e2e-password',", "passphrase:'e2e-passphrase',", + "idDerivationVersion:1,", + `idDerivationKey:${JSON.stringify(idDerivationKey)},`, "showVerboseLog:true,", "},true);", "await core.services.setting.saveSettingData();", @@ -64,6 +71,7 @@ async function main(): Promise { } const vault = await createTemporaryVault(); + const idDerivationKey = await deriveIdKey("setting-markdown-export-independent-id-key-fixture"); let session: ObsidianLiveSyncSession | undefined; try { console.log(`Using Obsidian executable: ${binary}`); @@ -77,19 +85,39 @@ async function main(): Promise { }); // The export is available while an unconfigured Vault remains outside // application readiness; the session helper has already loaded the plug-in. - await configureSettingMarkdown(cli.binary, session.cliEnv); + await configureSettingMarkdown(cli.binary, session.cliEnv, idDerivationKey); const content = await waitForFileContaining(vault.path, settingPath, [ (value) => value.includes("````yaml:livesync-setting"), (value) => value.includes(`settingSyncFile: ${settingPath}`), (value) => value.includes("showVerboseLog: true"), ]); + const persisted = JSON.parse( + await readFile(join(vault.path, ".obsidian", "plugins", "obsidian-livesync", "data.json"), "utf-8") + ) as { + idDerivationVersion?: unknown; + idDerivationKey?: unknown; + encryptedIdDerivationKey?: unknown; + }; + assertEqual(persisted.idDerivationVersion, 1, "The independent ID key fixture was not persisted."); + assertEqual(persisted.idDerivationKey, "", "The independent ID key was stored in plain text locally."); + const encryptedIdDerivationKey = persisted.encryptedIdDerivationKey; + if (typeof encryptedIdDerivationKey !== "string" || encryptedIdDerivationKey.length === 0) { + throw new Error("The independent ID key fixture was not saved in encrypted local settings."); + } + assertEqual( content.includes("couchDB_PASSWORD: e2e-password"), false, "Credential leaked into setting Markdown." ); assertEqual(content.includes("passphrase: e2e-passphrase"), false, "Passphrase leaked into setting Markdown."); + assertEqual(content.includes(idDerivationKey), false, "Plaintext ID key leaked into setting Markdown."); + assertEqual( + content.includes(encryptedIdDerivationKey), + false, + "Encrypted ID key leaked into setting Markdown." + ); console.log(`Generated setting Markdown without credentials: ${settingPath}`); } finally { diff --git a/test/e2e-obsidian/scripts/two-vault-sync.ts b/test/e2e-obsidian/scripts/two-vault-sync.ts index dbb97913..e006f1a7 100644 --- a/test/e2e-obsidian/scripts/two-vault-sync.ts +++ b/test/e2e-obsidian/scripts/two-vault-sync.ts @@ -1,11 +1,16 @@ import { mkdir, readFile, rename as renameFilesystemPath, rm, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; +import { SALT_OF_PASSPHRASE } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { encryptString } from "@vrtmrz/livesync-commonlib/compat/encryption/stringEncryption"; +import { deriveIdKey } from "@vrtmrz/livesync-commonlib/settings"; +import { CENTRAL_COMPATIBILITY_REJECTION_REASONS } from "@vrtmrz/livesync-commonlib/replication"; import { evalObsidianJson } from "../runner/cli.ts"; import { assertCouchDbReachable, createCouchDbDatabase, deleteCouchDbDatabase, fetchAllCouchDbDocs, + fetchCouchDbLocalDocs, loadCouchDbConfig, makeUniqueDatabaseName, waitForCouchDbDocs, @@ -19,6 +24,7 @@ import { assertE2eCompatibilityReviewPending, configureCouchDb, createE2eCouchDbPluginData, + createE2eObsidianDeviceLocalState, prepareRemote, pushLocalChanges, resumeCompatibilityReview, @@ -442,7 +448,8 @@ async function renameNoteViaObsidian(cliBinary: string, env: NodeJS.ProcessEnv, async function startConfiguredSession( context: RunnerContext, vault: TemporaryVault, - overrides: Record = {} + overrides: Record = {}, + persistedOverrides: Record = overrides ): Promise { const couchDbSettings = { uri: context.couchDb.uri, @@ -456,7 +463,7 @@ async function startConfiguredSession( cliBinary: context.cliBinary, vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), - pluginData: createE2eCouchDbPluginData(couchDbSettings, overrides), + pluginData: createE2eCouchDbPluginData(couchDbSettings, persistedOverrides), }); context.activeSessions.add(session); try { @@ -970,6 +977,193 @@ async function runEncryptedRoundTrip( console.log("Two-vault encrypted note synchronisation round-tripped."); } +async function runIndependentIdRoundTrip( + context: RunnerContext, + vaultA: TemporaryVault, + vaultB: TemporaryVault +): Promise { + const source = "real-obsidian-e2e-independent-id-source"; + const key = await deriveIdKey(source); + const content = "# Shared content with an independent ID key.\n"; + const pathA = "E2E/independent-ids/from-a.md"; + const pathB = "E2E/independent-ids/from-b.md"; + const overrides = { + encrypt: true, + passphrase: "real-obsidian-e2e-independent-passphrase", + usePathObfuscation: true, + E2EEAlgorithm: "v2", + idDerivationVersion: 1, + idDerivationKey: key, + }; + const persistedOverrides = { + ...overrides, + idDerivationKey: "", + encryptedIdDerivationKey: await encryptString(key, `*${SALT_OF_PASSPHRASE}`), + }; + + let session = await startConfiguredSession(context, vaultA, overrides, persistedOverrides); + await writeNoteViaObsidian(context.cliBinary, session.cliEnv, pathA, content); + const entryA = await uploadNote(context, session, pathA); + if (entryA.children.length === 0) throw new Error("Independent ID mode produced no Chunks."); + await stopTrackedSession(context, session); + + session = await startConfiguredSession(context, vaultB, overrides, persistedOverrides); + await syncAndApply(context, session); + await waitForPathContent(vaultB.path, pathA, (received) => received === content); + const receivedA = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, pathA); + assertEqual(receivedA.id, entryA.id, "The second device did not preserve the obfuscated document ID."); + + await writeNoteViaObsidian(context.cliBinary, session.cliEnv, pathB, content); + const entryB = await uploadNote(context, session, pathB); + assertEqual( + JSON.stringify(entryB.children), + JSON.stringify(entryA.children), + "The second device did not reuse the same content-derived Chunk IDs." + ); + await stopTrackedSession(context, session); + + session = await startConfiguredSession(context, vaultA, overrides, persistedOverrides); + await syncAndApply(context, session); + await waitForPathContent(vaultA.path, pathB, (received) => received === content); + const receivedB = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, pathB); + assertEqual(receivedB.id, entryB.id, "The first device did not preserve the return document ID."); + await stopTrackedSession(context, session); + console.log("Two real Obsidian devices shared independent document and Chunk IDs in both directions."); + + for (const [label, candidateKey] of [ + ["different key", "cd".repeat(32)], + ["legacy IDs", ""], + ] as const) { + const remoteBefore = await fetchAllCouchDbDocs(context.couchDb, context.dbName); + const checkpointsBefore = await fetchCouchDbLocalDocs(context.couchDb, context.dbName); + const rejectedVault = await createTemporaryVault(); + let rejectedSession: ObsidianLiveSyncSession | undefined; + try { + rejectedSession = await startObsidianLiveSyncSession({ + binary: context.binary, + cliBinary: context.cliBinary, + vault: rejectedVault, + localStorageEntries: createE2eObsidianDeviceLocalState(rejectedVault.name), + pluginData: createE2eCouchDbPluginData( + { ...context.couchDb, dbName: context.dbName }, + { + ...overrides, + idDerivationVersion: candidateKey ? 1 : 0, + idDerivationKey: "", + encryptedIdDerivationKey: candidateKey + ? await encryptString(candidateKey, `*${SALT_OF_PASSPHRASE}`) + : "", + } + ), + }); + context.activeSessions.add(rejectedSession); + await waitForLiveSyncCoreReady(context.cliBinary, rejectedSession.cliEnv); + const unsentPath = "E2E/independent-ids/rejected.md"; + await writeNoteViaObsidian(context.cliBinary, rejectedSession.cliEnv, unsentPath, content); + await waitForLocalDatabaseEntry(context.cliBinary, rejectedSession.cliEnv, unsentPath); + const attempt = await evalObsidianJson<{ admitted: boolean; reason: string; replicated: boolean }>( + context.cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const replicator=core.services.replicator.getActiveReplicator();", + "const settings=core.services.setting.currentSettings();", + "let reason='';", + "const connection=await replicator.checkReplicationConnectivity(settings,false,false,false,false,undefined,(decision)=>{reason=decision.reason??'';});", + "if(connection) await connection.close();", + "const replicated=await core.services.replication.replicate(true);", + "return JSON.stringify({admitted:!!connection,reason,replicated:!!replicated});", + "})()", + ].join(""), + rejectedSession.cliEnv + ); + assertEqual(attempt.admitted, false, `CouchDB admitted ${label} for obfuscated document IDs.`); + assertEqual( + attempt.reason, + CENTRAL_COMPATIBILITY_REJECTION_REASONS.ID_DERIVATION_MISMATCH, + `CouchDB rejected ${label} for an unrelated reason.` + ); + assertEqual(attempt.replicated, false, `Ordinary replication accepted ${label}.`); + assertEqual( + await pathExists(rejectedVault.path, pathA), + false, + "A rejected device received a remote note." + ); + await stopTrackedSession(context, rejectedSession); + rejectedSession = undefined; + assertEqual( + JSON.stringify(await fetchAllCouchDbDocs(context.couchDb, context.dbName)), + JSON.stringify(remoteBefore), + `A rejected ${label} connection changed remote documents.` + ); + assertEqual( + JSON.stringify(await fetchCouchDbLocalDocs(context.couchDb, context.dbName)), + JSON.stringify(checkpointsBefore), + `A rejected ${label} connection changed remote checkpoints.` + ); + } finally { + if (rejectedSession) await stopTrackedSession(context, rejectedSession); + await rejectedVault.dispose(); + } + } + console.log("Ordinary CouchDB replication rejected different and legacy document ID keys without remote writes."); +} + +async function runDifferentChunkIdKeysRoundTrip( + context: RunnerContext, + vaultA: TemporaryVault, + vaultB: TemporaryVault +): Promise { + const keyA = await deriveIdKey("real-obsidian-e2e-chunk-source-a"); + const keyB = await deriveIdKey("real-obsidian-e2e-chunk-source-b"); + const content = "# Shared content with different Chunk ID keys.\n"; + const pathA = "E2E/chunk-id-keys/from-a.md"; + const pathB = "E2E/chunk-id-keys/from-b.md"; + const commonSettings = { + encrypt: true, + passphrase: "real-obsidian-e2e-chunk-passphrase", + usePathObfuscation: false, + E2EEAlgorithm: "v2", + idDerivationVersion: 1, + }; + const settingsFor = (key: string) => ({ ...commonSettings, idDerivationKey: key }); + const persistedSettingsFor = async (key: string) => ({ + ...settingsFor(key), + idDerivationKey: "", + encryptedIdDerivationKey: await encryptString(key, `*${SALT_OF_PASSPHRASE}`), + }); + const persistedA = await persistedSettingsFor(keyA); + const persistedB = await persistedSettingsFor(keyB); + + let session = await startConfiguredSession(context, vaultA, settingsFor(keyA), persistedA); + await writeNoteViaObsidian(context.cliBinary, session.cliEnv, pathA, content); + const entryA = await uploadNote(context, session, pathA); + if (entryA.children.length === 0) throw new Error("The first device produced no Chunks."); + await stopTrackedSession(context, session); + + session = await startConfiguredSession(context, vaultB, settingsFor(keyB), persistedB); + await syncAndApply(context, session); + await waitForPathContent(vaultB.path, pathA, (received) => received === content); + const receivedA = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, pathA); + assertEqual(receivedA.id, entryA.id, "The second device changed the visible document ID."); + + await writeNoteViaObsidian(context.cliBinary, session.cliEnv, pathB, content); + const entryB = await uploadNote(context, session, pathB); + if (entryB.children.length === 0) throw new Error("The second device produced no Chunks."); + if (JSON.stringify(entryB.children) === JSON.stringify(entryA.children)) { + throw new Error("Different ID keys unexpectedly generated the same Chunk IDs."); + } + await stopTrackedSession(context, session); + + session = await startConfiguredSession(context, vaultA, settingsFor(keyA), persistedA); + await syncAndApply(context, session); + await waitForPathContent(vaultA.path, pathB, (received) => received === content); + const receivedB = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, pathB); + assertEqual(receivedB.id, entryB.id, "The first device changed the return document ID."); + await stopTrackedSession(context, session); + console.log("Two real Obsidian devices exchanged notes with different Chunk ID keys and visible document paths."); +} + async function runMarkdownAutoMerge( context: RunnerContext, vaultA: TemporaryVault, @@ -1094,10 +1288,9 @@ async function runConflictTimeStorageOperations( showMergeDialogOnlyOnActive: true, handleFilenameCaseSensitive: false, }; - const baseContent = Object.fromEntries(paths.map((path) => [path, `# Conflict operation\n\nBase for ${path}.\n`])) as Record< - (typeof paths)[number], - string - >; + const baseContent = Object.fromEntries( + paths.map((path) => [path, `# Conflict operation\n\nBase for ${path}.\n`]) + ) as Record<(typeof paths)[number], string>; const leftContent = Object.fromEntries( paths.map((path) => [path, `${baseContent[path]}\nEdit made on Vault A.\n`]) ) as Record<(typeof paths)[number], string>; @@ -1138,7 +1331,9 @@ async function runConflictTimeStorageOperations( const initialBranchRevisions = new Map>(); for (const path of paths) { const state = await waitForFileConflict(context.cliBinary, session.cliEnv, path); - const displayedBranch = state.branches.find((branch) => branch.content === rightContent[path] && !branch.deleted); + const displayedBranch = state.branches.find( + (branch) => branch.content === rightContent[path] && !branch.deleted + ); if (!displayedBranch) { throw new Error(`Could not identify the branch displayed by Vault B: ${path}; ${JSON.stringify(state)}`); } @@ -1179,12 +1374,7 @@ async function runConflictTimeStorageOperations( "A conflict-time deletion did not extend the displayed revision." ); - await renameNoteViaObsidian( - context.cliBinary, - session.cliEnv, - conflictCaseFromPath, - conflictCaseToPath - ); + await renameNoteViaObsidian(context.cliBinary, session.cliEnv, conflictCaseFromPath, conflictCaseToPath); const caseRenamedBranch = await waitForConflictBranch( context.cliBinary, session.cliEnv, @@ -1225,12 +1415,7 @@ async function runConflictTimeStorageOperations( "A conflict-time case-only rename did not record the new displayed revision." ); - await renameNoteViaObsidian( - context.cliBinary, - session.cliEnv, - conflictRenameFromPath, - conflictRenameToPath - ); + await renameNoteViaObsidian(context.cliBinary, session.cliEnv, conflictRenameFromPath, conflictRenameToPath); const renamedTarget = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, conflictRenameToPath); const renamedSourceDeletion = await waitForConflictBranch( context.cliBinary, @@ -1376,10 +1561,13 @@ async function main(): Promise { const couchDb = await loadCouchDbConfig(); const dbName = makeUniqueDatabaseName(couchDb.dbPrefix, "two-vault-sync"); const encryptedDbName = makeUniqueDatabaseName(couchDb.dbPrefix, "two-vault-sync-e2ee"); + const independentDbName = makeUniqueDatabaseName(couchDb.dbPrefix, "two-vault-sync-independent-ids"); const vaultA = await createTemporaryVault(); const vaultB = await createTemporaryVault(); const encryptedVaultA = await createTemporaryVault(); const encryptedVaultB = await createTemporaryVault(); + const independentVaultA = await createTemporaryVault(); + const independentVaultB = await createTemporaryVault(); const context: RunnerContext = { binary, cliBinary: cli.binary, @@ -1396,11 +1584,20 @@ async function main(): Promise { reviewedVaults: new Set(), activeSessions: new Set(), }; + const independentContext: RunnerContext = { + binary, + cliBinary: cli.binary, + couchDb, + dbName: independentDbName, + reviewedVaults: new Set(), + activeSessions: new Set(), + }; try { await assertCouchDbReachable(couchDb); await createCouchDbDatabase(couchDb, dbName); await createCouchDbDatabase(couchDb, encryptedDbName); + await createCouchDbDatabase(couchDb, independentDbName); console.log(`Using Obsidian executable: ${binary}`); console.log(`Temporary vault A: ${vaultA.path}`); @@ -1409,11 +1606,13 @@ async function main(): Promise { console.log(`Temporary encrypted CouchDB database: ${encryptedDbName}`); const onlyParentCaseDeletion = process.env.E2E_OBSIDIAN_ONLY_PARENT_CASE_DELETION === "true"; + const onlyIndependentIds = process.env.E2E_OBSIDIAN_ONLY_INDEPENDENT_IDS === "true"; + const onlyDifferentChunkIdKeys = process.env.E2E_OBSIDIAN_ONLY_DIFFERENT_CHUNK_ID_KEYS === "true"; if (onlyParentCaseDeletion) { await runParentCaseDeletionProtection(context, vaultA, vaultB); } const onlyConflictOperations = process.env.E2E_OBSIDIAN_ONLY_CONFLICT_OPERATIONS === "true"; - if (!onlyParentCaseDeletion && !onlyConflictOperations) { + if (!onlyParentCaseDeletion && !onlyConflictOperations && !onlyIndependentIds && !onlyDifferentChunkIdKeys) { await runCreateUpdateDelete(context, vaultA, vaultB); await runRename(context, vaultA, vaultB); await runCaseOnlyRename(context, vaultA, vaultB); @@ -1427,17 +1626,27 @@ async function main(): Promise { ) { await runConflictTimeStorageOperations(context, vaultA, vaultB); } - if (!onlyParentCaseDeletion && !onlyConflictOperations) { + if (!onlyParentCaseDeletion && !onlyConflictOperations && !onlyIndependentIds && !onlyDifferentChunkIdKeys) { await runTargetMismatch(context, vaultA, vaultB); await runEncryptedRoundTrip(encryptedContext, encryptedVaultA, encryptedVaultB); } + if (!onlyParentCaseDeletion && !onlyConflictOperations) { + if (onlyDifferentChunkIdKeys) { + await runDifferentChunkIdKeysRoundTrip(independentContext, independentVaultA, independentVaultB); + } else { + await runIndependentIdRoundTrip(independentContext, independentVaultA, independentVaultB); + } + } } finally { await stopTrackedSessions(context); await stopTrackedSessions(encryptedContext); + await stopTrackedSessions(independentContext); await vaultA.dispose(); await vaultB.dispose(); await encryptedVaultA.dispose(); await encryptedVaultB.dispose(); + await independentVaultA.dispose(); + await independentVaultB.dispose(); if (process.env.E2E_OBSIDIAN_KEEP_COUCHDB !== "true") { await deleteCouchDbDatabase(couchDb, dbName).catch((error: unknown) => { console.warn(error instanceof Error ? error.message : error); @@ -1445,6 +1654,9 @@ async function main(): Promise { await deleteCouchDbDatabase(couchDb, encryptedDbName).catch((error: unknown) => { console.warn(error instanceof Error ? error.message : error); }); + await deleteCouchDbDatabase(couchDb, independentDbName).catch((error: unknown) => { + console.warn(error instanceof Error ? error.message : error); + }); } } } diff --git a/updates.md b/updates.md index aec7077d..f11a6b45 100644 --- a/updates.md +++ b/updates.md @@ -16,6 +16,9 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi #### New Feature +- An optional saved ID key can generate encrypted Chunk IDs and obfuscated Metadata document IDs independently of the current E2EE passphrase. + - New Vaults use a random key by default; existing Vaults keep their current ID configuration by default. You can also derive a key from the current E2EE passphrase, enter a separate source, or import a recovery code. The source is not retained; the saved key can be revealed locally as a recovery code. + - The saved key stays in place when the E2EE passphrase changes or E2EE is turned off. Share it with another device through a protected Setup URI. Changing document IDs on an existing remote requires the usual Rebuild and Fetch procedure. - We can now keep the file properties used by Hidden File Sync and Customisation Sync private in CouchDB. - **Encrypt internal file Properties** extends E2EE V2 and Property Encryption to their paths, times, sizes, and Chunk references. - Existing configurations keep this preference disabled. New Vaults enable it for use when the required encryption settings are active. @@ -23,6 +26,9 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi - We can now see which unsupported feature prevents a client from synchronising with CouchDB. - Clients check the features required by the remote before transferring data or resetting the local database for Fast Fetch. Receiving an unsupported requirement also stops active replication. +- We can now compare ID generation performance on a desktop or mobile device through **Open review harness**, available with the developers' debug tools enabled. + - The copied report includes legacy and independent ID timings and, where available, approximate JavaScript heap samples. The measurement uses fixed test data and keeps our Vault and settings unchanged. + #### Fixed - We can now keep using an E2EE passphrase beginning with `%` after restarting Obsidian. (#1221) diff --git a/utils/couchdb/livesync-commonlib.ts b/utils/couchdb/livesync-commonlib.ts index b1a3a30a..b17ec92a 100644 --- a/utils/couchdb/livesync-commonlib.ts +++ b/utils/couchdb/livesync-commonlib.ts @@ -1,5 +1,5 @@ // Keep CouchDB database-version negotiation isolated from Setup URI generation. // The exact release must match utils/livesync-commonlib-version.ts; the setup // tool suite checks every static specifier before release. -export { checkRemoteVersion } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/pouchdb/negotiation"; -export { PouchDB } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/pouchdb/pouchdb-browser"; +export { checkRemoteVersion } from "npm:@vrtmrz/livesync-commonlib@0.1.32/compat/pouchdb/negotiation"; +export { PouchDB } from "npm:@vrtmrz/livesync-commonlib@0.1.32/compat/pouchdb/pouchdb-browser"; diff --git a/utils/flyio/deno.lock b/utils/flyio/deno.lock index 7ca3f1f5..6b9f40b0 100644 --- a/utils/flyio/deno.lock +++ b/utils/flyio/deno.lock @@ -1,7 +1,7 @@ { "version": "5", "specifiers": { - "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4": "0.1.0-rc.4" + "npm:@vrtmrz/livesync-commonlib@0.1.32": "0.1.32" }, "npm": { "@aws-sdk/checksums@3.1000.18": { @@ -284,8 +284,8 @@ "@trystero-p2p/core" ] }, - "@vrtmrz/livesync-commonlib@0.1.0-rc.4": { - "integrity": "sha512-u4FdbjnYg7lAf38z7eUv4eq4vxEdrl4rFMxiDZiJ7T701awKiflkXGIJQnaHdLaMa3zkRBU15qqEo7GtextmlA==", + "@vrtmrz/livesync-commonlib@0.1.32": { + "integrity": "sha512-gzjhd7bg+DKHhd/24WGxBM7557wsw3HJkc0YjKll1n8/8vuhqPnlLuZmM33fj+4bv9nGFUnDlnoVowpQrTns6w==", "dependencies": [ "@aws-sdk/client-s3", "@smithy/fetch-http-handler", @@ -509,8 +509,8 @@ "whatwg-url" ] }, - "octagonal-wheels@0.1.51": { - "integrity": "sha512-KTlfqKPjobHJg/t3A539srnFf+VHr1aXkHSmsNDDpiI5UFC7FamZ95dWpJfGE2EI/HULR5hveQDgkazmz8SAcg==", + "octagonal-wheels@0.1.54": { + "integrity": "sha512-Je3ancYhjKX7UY2K19T/qTjG8C9nK8YVrACr5naIf78mN4bbjQkYyWmlj+ooifV/moWVsQrp4fEWz/7mv6It3A==", "dependencies": [ "idb" ] diff --git a/utils/flyio/generate_setupuri.test.ts b/utils/flyio/generate_setupuri.test.ts index ec36f369..7c35f03e 100644 --- a/utils/flyio/generate_setupuri.test.ts +++ b/utils/flyio/generate_setupuri.test.ts @@ -35,6 +35,13 @@ Deno.test("generates a current self-hosted Setup URI through the published Commo const decoded = await decodeSettingsFromSetupURI(setupURI, "setup-secret"); assert(decoded, "Commonlib could not decode the generated Setup URI"); const effectiveSettings = { ...DEFAULT_SETTINGS, ...decoded }; + const recoveryCode = stdout.match(/sls-id-v1:[0-9a-f]{64}/u)?.[0]; + assert(recoveryCode, "the generator did not print an ID recovery code"); + assert( + (effectiveSettings as typeof effectiveSettings & { idDerivationKey?: string }).idDerivationKey === + recoveryCode.slice("sls-id-v1:".length), + "the CouchDB Setup URI did not contain the generated ID key", + ); assert( effectiveSettings.isConfigured, "the CouchDB Setup URI left the imported device unconfigured", diff --git a/utils/livesync-commonlib-version.ts b/utils/livesync-commonlib-version.ts index 945f0039..47913bdd 100644 --- a/utils/livesync-commonlib-version.ts +++ b/utils/livesync-commonlib-version.ts @@ -2,4 +2,4 @@ // Commonlib registry release. Static npm specifiers cannot interpolate this // value, so livesync-commonlib-version.test.ts verifies the domain-specific // facades against it. -export const LIVESYNC_COMMONLIB_VERSION = "0.1.0-rc.4"; +export const LIVESYNC_COMMONLIB_VERSION = "0.1.32"; diff --git a/utils/readme.md b/utils/readme.md index df4c3c64..894e6baf 100644 --- a/utils/readme.md +++ b/utils/readme.md @@ -31,6 +31,8 @@ Authentication and other non-retryable HTTP failures stop immediately. Network a The existing `flyio/generate_setupuri.ts` path remains a CouchDB-only compatibility wrapper for the Fly.io deployment script. +The generator creates a fresh random ID key by default and includes it in the encrypted Setup URI. It prints a tagged `sls-id-v1:` recovery code. Set `id_recovery_code` to that code when generating another URI for the same Vault; a new run without it creates a different key. This restores only the ID key: reuse the original connection details too. For P2P, provide the original `p2p_room_id` and `p2p_passphrase` because omitted values are generated afresh. Set `id_mode=legacy` to generate a URI with the previous ID behaviour. `id_mode=legacy` and `id_recovery_code` cannot be combined. Keep the recovery code private and retain it if every device might be lost. + ### CouchDB ```sh diff --git a/utils/setup/generate_setup_uri.test.ts b/utils/setup/generate_setup_uri.test.ts index 725725bf..2dea3dbf 100644 --- a/utils/setup/generate_setup_uri.test.ts +++ b/utils/setup/generate_setup_uri.test.ts @@ -26,6 +26,20 @@ Deno.test("generates an Object Storage Setup URI with a selected S3 profile", as ); assert(decoded, "Commonlib could not decode the Object Storage Setup URI"); const effective = { ...DEFAULT_SETTINGS, ...decoded }; + const recoveryCode = generated.idRecoveryCode; + assert( + typeof recoveryCode === "string" && recoveryCode.startsWith("sls-id-v1:"), + "the generator did not return an ID recovery code", + ); + assert( + (effective as typeof effective & { idDerivationVersion?: number }).idDerivationVersion === 1, + "the Setup URI did not enable independent IDs", + ); + assert( + (effective as typeof effective & { idDerivationKey?: string }).idDerivationKey === + recoveryCode.slice("sls-id-v1:".length), + "the Setup URI did not contain the generated ID key", + ); assert( effective.isConfigured, "the Setup URI left the imported device unconfigured", @@ -74,6 +88,11 @@ Deno.test("generates a random-room P2P Setup URI without copying a device identi ); assert(decoded, "Commonlib could not decode the P2P Setup URI"); const effective = { ...DEFAULT_SETTINGS, ...decoded }; + assert( + (effective as typeof effective & { idDerivationKey?: string }).idDerivationKey === + generated.idRecoveryCode?.slice("sls-id-v1:".length), + "the P2P Setup URI did not contain the generated ID key", + ); assert( /^\d{3}-\d{3}-\d{3}-[a-z0-9]{3}$/.test(effective.P2P_roomID), "Commonlib did not generate the expected random room ID", @@ -121,3 +140,46 @@ Deno.test("generates a random-room P2P Setup URI without copying a device identi "the selected profile was not a P2P connection URI", ); }); + +Deno.test("reuses the ID key from a recovery code and permits explicit legacy IDs", async () => { + const environment = { + remote_type: "p2p", + passphrase: "vault-secret", + uri_passphrase: "setup-secret", + }; + const first = await generateSetupURI(environment); + const second = await generateSetupURI({ ...environment, id_recovery_code: first.idRecoveryCode }); + const independentlyGenerated = await generateSetupURI(environment); + assert(second.idRecoveryCode === first.idRecoveryCode, "the recovery code changed on repeat generation"); + assert(independentlyGenerated.idRecoveryCode !== first.idRecoveryCode, "the default ID key was reused"); + const repeatedSettings = await decodeSettingsFromSetupURI(second.setupURI, second.setupPassphrase); + assert(repeatedSettings, "the repeated Setup URI could not be decoded"); + assert( + (repeatedSettings as typeof repeatedSettings & { idDerivationKey?: string }).idDerivationKey === + first.idRecoveryCode?.slice("sls-id-v1:".length), + "the recovery code did not restore the original ID key", + ); + + const legacy = await generateSetupURI({ ...environment, id_mode: "legacy" }); + const decoded = await decodeSettingsFromSetupURI(legacy.setupURI, legacy.setupPassphrase); + assert(decoded, "the legacy Setup URI could not be decoded"); + assert(legacy.idRecoveryCode === undefined, "legacy mode returned an ID recovery code"); + assert( + (decoded as typeof decoded & { idDerivationVersion?: number }).idDerivationVersion !== 1, + "legacy mode enabled independent IDs", + ); + let rejected = false; + try { + await generateSetupURI({ ...environment, id_recovery_code: "sls-id-v1:wrong" }); + } catch { + rejected = true; + } + assert(rejected, "an invalid recovery code was accepted"); + rejected = false; + try { + await generateSetupURI({ ...environment, id_mode: "legacy", id_recovery_code: first.idRecoveryCode }); + } catch { + rejected = true; + } + assert(rejected, "legacy mode silently ignored a recovery code"); +}); diff --git a/utils/setup/generate_setup_uri.ts b/utils/setup/generate_setup_uri.ts index 07c4f1cd..d090d289 100644 --- a/utils/setup/generate_setup_uri.ts +++ b/utils/setup/generate_setup_uri.ts @@ -19,6 +19,36 @@ export interface GeneratedSetupURI { remoteType: SetupRemoteType; setupURI: string; setupPassphrase: string; + idRecoveryCode?: string; +} + +const ID_RECOVERY_CODE_PREFIX = "sls-id-v1:"; +const ID_RECOVERY_CODE_PATTERN = /^sls-id-v1:([0-9a-f]{64})$/u; + +function generateRandomIdKey(): string { + const bytes = crypto.getRandomValues(new Uint8Array(32)); + return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +function configureIdDerivation( + settings: ObsidianLiveSyncSettings, + environment: SetupGeneratorEnvironment, +): string | undefined { + const mode = environment.id_mode?.trim().toLowerCase() || "random"; + if (mode !== "random" && mode !== "legacy") { + throw new Error("id_mode must be random or legacy"); + } + const suppliedCode = environment.id_recovery_code?.trim(); + if (mode === "legacy") { + if (suppliedCode) throw new Error("id_recovery_code cannot be used with id_mode=legacy"); + return undefined; + } + const key = suppliedCode + ? ID_RECOVERY_CODE_PATTERN.exec(suppliedCode)?.[1] + : generateRandomIdKey(); + if (!key) throw new Error("id_recovery_code must be a valid sls-id-v1 recovery code"); + Object.assign(settings, { idDerivationVersion: 1, idDerivationKey: key }); + return `${ID_RECOVERY_CODE_PREFIX}${key}`; } function requireValue( @@ -166,11 +196,12 @@ export async function generateSetupURI( const setupPassphrase = environment.uri_passphrase?.trim() || generateSecret(); const { remoteType, settings } = createSetupSettings(environment); + const idRecoveryCode = configureIdDerivation(settings, environment); const setupURI = await encodeSettingsToSetupURI(settings, setupPassphrase, [ "pluginSyncExtendedSetting", "doNotUseFixedRevisionForChunks", ], true); - return { remoteType, setupURI: setupURI.trim(), setupPassphrase }; + return { remoteType, setupURI: setupURI.trim(), setupPassphrase, idRecoveryCode }; } export async function runSetupURIGenerator( @@ -183,6 +214,10 @@ export async function runSetupURIGenerator( generated.setupPassphrase, ); console.log("This passphrase is never shown again, so store it safely."); + if (generated.idRecoveryCode) { + console.log("ID recovery code:", generated.idRecoveryCode); + console.log("Use id_recovery_code with this value and reuse the same remote settings when generating another Setup URI for the same Vault."); + } console.log(generated.setupURI); } diff --git a/utils/setup/livesync-commonlib.ts b/utils/setup/livesync-commonlib.ts index d8091d4d..2443266c 100644 --- a/utils/setup/livesync-commonlib.ts +++ b/utils/setup/livesync-commonlib.ts @@ -4,9 +4,9 @@ export { decodeSettingsFromSetupURI, encodeSettingsToSetupURI, -} from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/API/processSetting"; -export { generateP2PRoomId } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/common/utils"; -export { upsertRemoteConfigurationInPlace } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/remote-configurations"; +} from "npm:@vrtmrz/livesync-commonlib@0.1.32/compat/API/processSetting"; +export { generateP2PRoomId } from "npm:@vrtmrz/livesync-commonlib@0.1.32/compat/common/utils"; +export { upsertRemoteConfigurationInPlace } from "npm:@vrtmrz/livesync-commonlib@0.1.32/remote-configurations"; export { createNewVaultSettings, DEFAULT_SETTINGS, @@ -14,5 +14,5 @@ export { PREFERRED_BASE, PREFERRED_JOURNAL_SYNC, PREFERRED_SETTING_SELF_HOSTED, -} from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/settings"; -export type { ObsidianLiveSyncSettings } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/settings"; +} from "npm:@vrtmrz/livesync-commonlib@0.1.32/settings"; +export type { ObsidianLiveSyncSettings } from "npm:@vrtmrz/livesync-commonlib@0.1.32/settings"; From 6b3489cfd9fc8b9cffd002e3e839121706355f92 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Tue, 29 Sep 2026 14:36:59 +0000 Subject: [PATCH 28/58] Clarify build and published artefact validation boundaries --- test/e2e-obsidian/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index e8175416..75790a0a 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -273,7 +273,9 @@ Build the current plug-in once, then run these focused scenarios sequentially wi The setup-tool contract suite also checks ID recovery and explicit legacy IDs in both URI modes. `dialog-mounts` covers the availability dialogue and setup choices on desktop and emulated mobile. These automated scenarios remove the need to repeat every decision path manually during BRAT acceptance. -BRAT acceptance still validates the exact published artefacts: install or update through BRAT, cold-start Obsidian, and exchange one note in each direction. On a physical mobile device, include one Setup URI or QR hand-off and check the displayed instructions, principal controls, and responsiveness. Camera capture, operating-system dispatch, native mobile performance, and the published installation path are outside this local E2E coverage; emulated mobile establishes layout and interaction only. +BRAT acceptance validates the exact published artefacts: install or update through BRAT, cold-start Obsidian, and exchange one note in each direction. Build behaviour can be checked before publication using the exact reviewed build; record its identity and avoid repeating the same decision paths during BRAT acceptance. + +A short physical-device check is optional when a specific concern remains about localised time text, input, clipboard interaction, or responsiveness. The camera and operating-system dispatch paths are unchanged by this integration and do not require routine revalidation. Emulated mobile establishes layout and interaction, but does not establish native device performance or verify the published installation path. ### Environment variables From de2ebaeea5d29c53c05bcb32db0ec70f93b6428d Mon Sep 17 00:00:00 2001 From: Kim Jansheden Date: Tue, 29 Sep 2026 19:39:20 +0200 Subject: [PATCH 29/58] Resume held replication results on application readiness Trigger queued result processing on Commonlib readiness while preserving explicit suspension. Cover both transitions with focused tests. --- .../replication/ReplicateResultProcessor.ts | 9 +++++- .../ReplicateResultProcessor.unit.spec.ts | 29 +++++++++++++++++++ src/serviceFeatures/replication/index.ts | 4 +++ .../replicationFeature.unit.spec.ts | 19 ++++++++++++ updates.md | 6 ++++ 5 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/serviceFeatures/replication/ReplicateResultProcessor.ts b/src/serviceFeatures/replication/ReplicateResultProcessor.ts index f0010c96..9474e58e 100644 --- a/src/serviceFeatures/replication/ReplicateResultProcessor.ts +++ b/src/serviceFeatures/replication/ReplicateResultProcessor.ts @@ -108,8 +108,15 @@ export class ReplicateResultProcessor { } public resume() { this._suspended = false; + this.continueHeldDocuments(); + } + /** + * Continue the queued documents which were held, for example while the application was not ready. + * An explicit suspension, by `suspend()` or by the settings, remains in effect. + */ + public continueHeldDocuments() { this.updateProcessingActivity(); - fireAndForget(() => this.runProcessQueue()); + this.triggerProcessQueue(); } // Whether the processing is suspended diff --git a/src/serviceFeatures/replication/ReplicateResultProcessor.unit.spec.ts b/src/serviceFeatures/replication/ReplicateResultProcessor.unit.spec.ts index 2ea388ed..4e992692 100644 --- a/src/serviceFeatures/replication/ReplicateResultProcessor.unit.spec.ts +++ b/src/serviceFeatures/replication/ReplicateResultProcessor.unit.spec.ts @@ -207,6 +207,35 @@ describe("ReplicateResultProcessor", () => { expect(isReady).toHaveBeenCalledOnce(); }); + it("applies documents held before readiness once the application becomes ready", async () => { + const { isReady, processor, processSynchroniseResult } = setup({ applicationReady: false }); + processor.enqueueAll([note("held")]); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(processSynchroniseResult).not.toHaveBeenCalled(); + expect(processor["_queuedChanges"]).toHaveLength(1); + + isReady.mockReturnValue(true); + processor.continueHeldDocuments(); + + await vi.waitFor(() => expect(processSynchroniseResult).toHaveBeenCalledOnce()); + expect(processor["_queuedChanges"]).toHaveLength(0); + }); + + it("keeps an explicit suspension when the application becomes ready", async () => { + const { isReady, processor, processSynchroniseResult } = setup({ applicationReady: false }); + processor.suspend(); + processor.enqueueAll([note("held")]); + + isReady.mockReturnValue(true); + processor.continueHeldDocuments(); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(processSynchroniseResult).not.toHaveBeenCalled(); + expect(processor["_queuedChanges"]).toHaveLength(1); + + processor.resume(); + await vi.waitFor(() => expect(processSynchroniseResult).toHaveBeenCalledOnce()); + }); + it("applies results in remediation mode, which never reports readiness", () => { const { processor } = setup({ applicationReady: false, diff --git a/src/serviceFeatures/replication/index.ts b/src/serviceFeatures/replication/index.ts index 74a4f1e4..37eb5f52 100644 --- a/src/serviceFeatures/replication/index.ts +++ b/src/serviceFeatures/replication/index.ts @@ -4,6 +4,7 @@ import { UnresolvedErrorManager } from "@vrtmrz/livesync-commonlib/compat/servic import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context"; import { fireAndForget } from "octagonal-wheels/promises"; import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore"; +import { EVENT_APPLICATION_READY } from "@/common/events"; import { createAutomaticReplicationTriggers } from "./automaticTriggers"; import { createCentralCompatibilityRecovery } from "./centralCompatibilityRecovery"; import { createOnlineReplicationPreflight, createSecuritySeedPreflight } from "./preflight"; @@ -102,6 +103,9 @@ export function useReplicationFeature resultProcessor.restoreFromSnapshotOnce()); return Promise.resolve(true); }); + // Commonlib emits this each time it establishes readiness. Documents held until then, such as those restored + // from the snapshot or received during a fetch, continue from here. + services.context.events.onEvent(EVENT_APPLICATION_READY, () => resultProcessor.continueHeldDocuments()); services.appLifecycle.onSettingLoaded.addHandler(initialiseAutomaticReplicationTriggers); services.replication.parseSynchroniseResult.addHandler((documents) => { resultProcessor.enqueueAll(documents); diff --git a/src/serviceFeatures/replication/replicationFeature.unit.spec.ts b/src/serviceFeatures/replication/replicationFeature.unit.spec.ts index fc87dc7a..eb3ff5f3 100644 --- a/src/serviceFeatures/replication/replicationFeature.unit.spec.ts +++ b/src/serviceFeatures/replication/replicationFeature.unit.spec.ts @@ -3,7 +3,9 @@ import { createServiceContext } from "@vrtmrz/livesync-commonlib/context"; import { VERSIONING_DOCID, type EntryDoc } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { REMOTE_FEATURE_GENERATION } from "@vrtmrz/livesync-commonlib/replication"; import { promiseWithResolvers } from "octagonal-wheels/promises"; +import { EVENT_APPLICATION_READY } from "@/common/events"; import { useReplicationFeature } from "./index"; +import { ReplicateResultProcessor } from "./ReplicateResultProcessor"; type BooleanHandler = (showMessage: boolean) => Promise; type ParseHandler = (documents: PouchDB.Core.ExistingDocument[]) => Promise; @@ -95,6 +97,7 @@ function setup(options: SetupOptions = {}) { return { beforeReplicateHandlers, centralRemoteHandlers, + context: services.context, createRemoteResource, dispose, get parseHandler() { @@ -168,6 +171,22 @@ describe("replication serviceFeature composition", () => { expect(createRemoteResource).toHaveBeenCalledOnce(); }); + it("continues held results when Commonlib establishes application readiness", () => { + const continueHeldDocuments = vi + .spyOn(ReplicateResultProcessor.prototype, "continueHeldDocuments") + .mockImplementation(() => undefined); + try { + const { context } = setup(); + expect(continueHeldDocuments).not.toHaveBeenCalled(); + + context.events.emitEvent(EVENT_APPLICATION_READY); + + expect(continueHeldDocuments).toHaveBeenCalledOnce(); + } finally { + continueHeldDocuments.mockRestore(); + } + }); + it("requests owner retirement without awaiting the transition from result application", async () => { const retirement = promiseWithResolvers(); const onCloseActiveReplication = vi.fn(() => retirement.promise); diff --git a/updates.md b/updates.md index f11a6b45..e9becde8 100644 --- a/updates.md +++ b/updates.md @@ -34,6 +34,12 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi - We can now keep using an E2EE passphrase beginning with `%` after restarting Obsidian. (#1221) - LiveSync encrypts it before saving the settings. If an earlier version saved it in plain text, re-enter the passphrase used to encrypt the existing data after updating. Treat that passphrase as exposed if the affected `data.json` was shared. +### Synchronisation and storage + +#### Fixed + +- Received changes held during start-up or a fetch are applied when LiveSync becomes ready, without waiting for another change or a settings save. **Suspend database reflecting** continues to hold changes (#1200). + ## 1.0.32 27th September, 2026 From ad5351b8f6affbb2186cc977ed0e37f7f5297ae8 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Wed, 30 Sep 2026 01:31:36 +0000 Subject: [PATCH 30/58] Use a link icon for joining a device --- src/common/messages/LiveSyncProvisionalMessages.ts | 2 +- .../features/SetupWizard/dialogs/OutroAskUserMode.svelte | 2 +- test/e2e-obsidian/scripts/object-storage-setup-uri-workflow.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/messages/LiveSyncProvisionalMessages.ts b/src/common/messages/LiveSyncProvisionalMessages.ts index f6f6ae7c..e4e60160 100644 --- a/src/common/messages/LiveSyncProvisionalMessages.ts +++ b/src/common/messages/LiveSyncProvisionalMessages.ts @@ -8,7 +8,7 @@ */ export const liveSyncProvisionalEnglishMessages = { "⚠️ Initialise or overwrite the remote": "⚠️ Initialise or overwrite the remote", - "📥 Join this device": "📥 Join this device", + "🔗 Join this device": "🔗 Join this device", "⚙️ Apply settings only (advanced)": "⚙️ Apply settings only (advanced)", "Configure TURN when a direct connection cannot be established or when you select TURN relay only.": "Configure TURN when a direct connection cannot be established or when you select TURN relay only.", diff --git a/src/modules/features/SetupWizard/dialogs/OutroAskUserMode.svelte b/src/modules/features/SetupWizard/dialogs/OutroAskUserMode.svelte index 0dad2b27..68ca63bd 100644 --- a/src/modules/features/SetupWizard/dialogs/OutroAskUserMode.svelte +++ b/src/modules/features/SetupWizard/dialogs/OutroAskUserMode.svelte @@ -59,7 +59,7 @@