diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 73c6849b..aa86409a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -63,9 +63,18 @@ jobs: - name: Rust cache uses: swatinem/rust-cache@v2 with: - workspaces: apps/desktop/src-tauri -> target + workspaces: | + apps/desktop/src-tauri -> target + crates/mydt -> target # SQLCipher + OpenSSL are vendored (rusqlite bundled-sqlcipher-vendored-openssl), # so the first run compiles them; rust-cache makes later runs fast. - name: cargo test run: cargo test --locked + + # Shared .mydt format crate + CLI (no Tauri deps, builds in seconds). + - name: mydt crate (format + CLI) + working-directory: crates/mydt + run: | + cargo test --locked + cargo build --locked --release --features cli diff --git a/.gitignore b/.gitignore index b174a5e0..ddd95f34 100644 --- a/.gitignore +++ b/.gitignore @@ -99,4 +99,11 @@ docs/superpowers/ # graft's local graph cache — regenerable, not committed (run `graft build`). graft/ # Local Linux build output (published to GitHub releases, never committed) -dist-linux/ \ No newline at end of file +dist-linux/ + +# Rust crates outside the Tauri shell +crates/*/target/ + +# Regenerated by `next dev` (agentRules); not project instructions +apps/desktop-ui/AGENTS.md +apps/desktop-ui/CLAUDE.md diff --git a/README.md b/README.md index 090d6c45..88c81cb2 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,7 @@ go straight from your machine to your database. | Tool | Description | |---|---| | **Password Manager** | Credential vault encrypted with a password only you know | +| **Secure Files** | Encrypt files and folders into masked `.mydt` objects in any folder you choose; browse, preview and export them after unlocking ([format spec](docs/MYDT_FORMAT.md), [`mydt` CLI](crates/mydt/README.md)) | | **Encryption Playground** | AES-GCM with a raw key or passphrase; encrypt/decrypt JSON bundles | | **JWT Decoder** | Decode JWT header, payload and expiry | | **Hash Generator** | MD5, SHA-1, SHA-256/384/512 digests for text or files | diff --git a/apps/desktop-ui/AGENTS.md b/apps/desktop-ui/AGENTS.md new file mode 100644 index 00000000..643577df --- /dev/null +++ b/apps/desktop-ui/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/apps/desktop-ui/CLAUDE.md b/apps/desktop-ui/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/apps/desktop-ui/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/apps/desktop-ui/messages/af.json b/apps/desktop-ui/messages/af.json index b5c44dfe..aa61f875 100644 --- a/apps/desktop-ui/messages/af.json +++ b/apps/desktop-ui/messages/af.json @@ -76,7 +76,8 @@ "tokenCounter": "Token-teller", "webhookTester": "Webhook-toetser", "websocketTester": "WebSocket-toetser", - "whoisLookup": "Whois-opsoek" + "whoisLookup": "Whois-opsoek", + "secureFiles": "Lêers" }, "Help": { "title": "Hulp en dokumentasie", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Wysig", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Herstel tans…", "restoreSuccess": "Rugsteun herstel", "restoreError": "Herstel het misluk — verkeerde wagfrase of ongeldige rugsteunlêer" + }, + "backupCodes": { + "title": "Rugsteunkodes", + "description": "Eenmalige kodes wat jou kluis herstel as jy jou meesterwagwoord vergeet.", + "remaining": "{remaining} van {total} kodes oor", + "none": "Geen rugsteunkodes gestoor nie.", + "hint": "'n Nuwe stel maak alle bestaande kodes ongeldig.", + "passwordPlaceholder": "Meesterwagwoord", + "regenerateButton": "Genereer nuwe kodes", + "generating": "Genereer tans…", + "wrongPassword": "Verkeerde meesterwagwoord", + "success": "Nuwe rugsteunkodes gegenereer", + "error": "Kon nie rugsteunkodes genereer nie", + "newCodesWarning": "Jy kan dit nie weer sien nie. Laai dit af of kopieer dit nou.", + "downloadButton": "Laai af", + "doneButton": "Ek het my kodes gestoor" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Whois-opsoek", "description": "Soek domein- en IP-registrasiebesonderhede op via RDAP." + }, + "secureFiles": { + "title": "Lêers", + "description": "Enkripteer lêers en vouers na gemaskerde .mydt-objekte; blaai en voorskou hulle nadat jy ontsluit het." } }, "tabs": { @@ -2278,7 +2300,8 @@ "responseCopied": "Antwoord na knipbord gekopieer", "codeCopied": "Kode na knipbord gekopieer", "copyFailed": "Failed to copy to clipboard", - "curlCopied": "cURL-opdrag gekopieer" + "curlCopied": "cURL-opdrag gekopieer", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Versamelings", @@ -2351,6 +2374,8 @@ "collectionsSidebar": { "title": "API-hulpbronne", "newCollection": "Nuwe versameling", + "openFolderCollection": "Maak vouer oop as versameling", + "folderCollection": "Vouerversameling", "tabCollections": "Versamelings", "tabHistory": "Geskiedenis", "noCollectionsTitle": "Geen versamelings", @@ -2392,6 +2417,7 @@ "placeholderNewEnv": "Nuwe omgewing", "variablesHeading": "{name}-veranderlikes", "addVariable": "Voeg veranderlike by", + "exportPostman": "Voer uit (Postman-omgewing)", "columnVariable": "Veranderlike", "columnValue": "Waarde", "placeholderKey": "Sleutel", @@ -2448,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5188,6 +5223,12 @@ "offline": "100% aflyn" }, "ToolSidebar": { + "expand": "Vou sybalk uit", + "collapse": "Vou sybalk in", + "resize": "Verstel sybalk se grootte", + "resetWidth": "Herstel sybalk se breedte", + "moreSections": "Meer afdelings", + "sections": "Sybalk-afdelings", "show": "Wys sybalk", "hide": "Versteek sybalk" }, @@ -5211,5 +5252,92 @@ "accept": "Deel anoniem", "decline": "Nee dankie" } + }, + "SecureFiles": { + "title": "Lêers", + "subtitle": "Lêers wat privaat bly totdat jy hulle ontsluit — geënkripteer op jou toestel, gemasker op skyf", + "desktopOnly": "Secure Files is in die rekenaartoepassing beskikbaar.", + "allFiles": "Alle lêers", + "overview": "Oorsig", + "statFiles": "Lêers", + "statFolders": "Vouers", + "statContent": "Inhoudgrootte", + "statContentHint": "Totale grootte van die oorspronklike lêers", + "statOnDisk": "Op skyf", + "statOverhead": "{size} enkripsie-oorhoofse koste", + "byType": "Volgens tipe", + "fileCount": "{count, plural, one {# lêer} other {# lêers}}", + "largestFiles": "Grootste lêers", + "recentlyAdded": "Onlangs bygevoeg", + "openContainingFolder": "Open die vouer wat dit bevat", + "types": { + "image": "Beelde", + "video": "Video", + "audio": "Klank", + "pdf": "PDF", + "archive": "Argiewe", + "code": "Kode", + "doc": "Dokumente", + "sheet": "Sigblaaie", + "file": "Ander" + }, + "breadcrumb": "Vouerpad", + "addFiles": "Voeg lêers by", + "addFolder": "Voeg vouer by", + "importFolder": "Voer vouer in", + "newFolder": "Nuwe vouer", + "lock": "Sluit", + "settings": "Bergingsinstellings", + "storageFolder": "Bergingsvouer", + "notSet": "Nie gestel nie", + "chooseFolder": "Kies vouer", + "changeFolder": "Verander vouer…", + "chooseFolderTitle": "Kies waar geënkripteerde lêers gestoor word", + "chooseFolderBody": "Kies enige vouer op hierdie toestel of op 'n eksterne skyf. Buite MyDevTools bevat dit net gemaskerde .mydt-lêers.", + "changeFolderTitle": "Skuif geënkripteerde lêers?", + "changeFolderBody": "Alle .mydt-lêers word van die huidige bergingsvouer geskuif na die een wat jy volgende kies.", + "folderSet": "Bergingsvouer gestel", + "folderMissingTitle": "Bergingsvouer nie gevind nie", + "folderMissingBody": "“{dir}” is nie beskikbaar nie — dit is dalk op 'n ontkoppelde skyf of kom van 'n herstelde rugsteun van 'n ander masjien. Koppel dit weer, of kies 'n nuwe vouer.", + "movedCount": "{count} geënkripteerde lêers geskuif", + "importedCount": "{count} lêers geënkripteer", + "emptyFolderAdded": "Vouer bygevoeg — dit het geen lêers gehad om te enkripteer nie", + "emptyTitle": "Nog geen lêers hier nie", + "emptyBody": "Voeg lêers of 'n hele vouer by. Elke lêer word geënkripteer na 'n .mydt-objek met 'n lukrake naam; die oorspronklike naam, tipe en inhoud is net hier sigbaar.", + "name": "Naam", + "size": "Grootte", + "modified": "Gewysig", + "actions": "Aksies", + "expand": "Vou oop", + "collapse": "Vou toe", + "preview": "Voorskou", + "noPreview": "Geen voorskou vir hierdie lêertipe nie — voer dit uit om dit in 'n ander toepassing te open.", + "export": "Voer uit", + "exportWarningTitle": "Voer 'n gedekripteerde kopie uit?", + "exportWarningBody": "Die lêer word ongeënkripteer geskryf na die plek wat jy kies. Enigiets op daardie toestel kan dit lees.", + "exported": "Gedekripteerde kopie uitgevoer", + "replace": "Vervang inhoud…", + "replaced": "Lêerinhoud vervang", + "rename": "Hernoem", + "fileName": "Lêernaam", + "move": "Skuif na vouer", + "targetFolder": "Vouer", + "targetFolderHint": "Gebruik skuinsstrepe vir geneste vouers, byvoorbeeld projek/config. Laat leeg vir die boonste vlak.", + "folderName": "Vouernaam", + "delete": "Vee uit", + "deleted": "Lêer uitgevee", + "deletedCount": "{count} lêers uitgevee", + "deleteFileTitle": "Vee hierdie lêer uit?", + "deleteFileBody": "“{name}” word permanent uit die geënkripteerde bergingsvouer verwyder.", + "deleteFolderTitle": "Vee hierdie vouer uit?", + "deleteFolderBody": "Elke lêer onder “{dir}” word permanent uit die geënkripteerde bergingsvouer verwyder.", + "unreadableCount": "Onleesbare lêers in die bergingsvouer: {count}", + "unreadableHint": "Hulle is met 'n ander meesterwagwoord geënkripteer, behoort aan 'n ander kluis, of is beskadig.", + "dismiss": "Maak toe", + "viewGrid": "Roosteraansig", + "viewList": "Lysaansig", + "footerNote": "Lêers tot 20 MB. Geënkripteer met Argon2id + XChaCha20-Poly1305; die sleutel bly net in geheue terwyl die kluis ontsluit is.", + "cancel": "Kanselleer", + "save": "Stoor" } } diff --git a/apps/desktop-ui/messages/ar.json b/apps/desktop-ui/messages/ar.json index 9a8aadc5..4e1ac6d2 100644 --- a/apps/desktop-ui/messages/ar.json +++ b/apps/desktop-ui/messages/ar.json @@ -76,7 +76,8 @@ "tokenCounter": "عدّاد الرموز", "webhookTester": "مختبِر Webhook", "websocketTester": "مختبِر WebSocket", - "whoisLookup": "بحث Whois" + "whoisLookup": "بحث Whois", + "secureFiles": "الملفات" }, "Help": { "title": "المساعدة والتوثيق", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "تعديل", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "جارٍ الاستعادة…", "restoreSuccess": "تمت استعادة النسخة الاحتياطية", "restoreError": "فشلت الاستعادة — عبارة مرور خاطئة أو ملف نسخة احتياطية غير صالح" + }, + "backupCodes": { + "title": "رموز النسخ الاحتياطي", + "description": "رموز تُستخدم لمرة واحدة لاستعادة خزنتك إذا نسيت كلمة المرور الرئيسية.", + "remaining": "بقي {remaining} من {total} رمزًا", + "none": "لا توجد رموز نسخ احتياطي مخزَّنة.", + "hint": "إنشاء مجموعة جديدة يُبطل جميع الرموز الحالية.", + "passwordPlaceholder": "كلمة المرور الرئيسية", + "regenerateButton": "إنشاء رموز جديدة", + "generating": "جارٍ الإنشاء…", + "wrongPassword": "كلمة المرور الرئيسية غير صحيحة", + "success": "تم إنشاء رموز نسخ احتياطي جديدة", + "error": "تعذر إنشاء رموز النسخ الاحتياطي", + "newCodesWarning": "لن تتمكن من رؤيتها مرة أخرى. نزّلها أو انسخها الآن.", + "downloadButton": "تنزيل", + "doneButton": "لقد حفظت رموزي" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "بحث Whois", "description": "ابحث عن تفاصيل تسجيل النطاق وعنوان IP عبر RDAP." + }, + "secureFiles": { + "title": "الملفات", + "description": "شفّر الملفات والمجلدات إلى كائنات ‎.mydt‎ مُقنَّعة الأسماء؛ تصفّحها وعاينها بعد فتح القفل." } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "تم أخذ عينة من {docs} مستند · {fields} حقل", "colField": "الحقل", "colTypes": "الأنواع", - "colCoverage": "التغطية" + "colCoverage": "التغطية", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "تم نسخ الاستجابة", "codeCopied": "تم نسخ الكود", "copyFailed": "فشل نسخ المحتوى إلى الحافظة", - "curlCopied": "تم نسخ أمر cURL" + "curlCopied": "تم نسخ أمر cURL", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "المجموعات", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "موارد API", "newCollection": "مجموعة جديدة", + "openFolderCollection": "فتح مجلد كمجموعة", + "folderCollection": "مجموعة مجلد", "tabCollections": "المجموعات", "tabHistory": "السجل", "noCollectionsTitle": "لا مجموعات", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "بيئة جديدة", "variablesHeading": "متغيرات {name}", "addVariable": "إضافة متغير", + "exportPostman": "تصدير (بيئة Postman)", "columnVariable": "متغير", "columnValue": "قيمة", "placeholderKey": "مفتاح", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100% دون اتصال" }, "ToolSidebar": { + "expand": "توسيع الشريط الجانبي", + "collapse": "طي الشريط الجانبي", + "resize": "تغيير حجم الشريط الجانبي", + "resetWidth": "إعادة تعيين عرض الشريط الجانبي", + "moreSections": "المزيد من الأقسام", + "sections": "أقسام الشريط الجانبي", "show": "إظهار الشريط الجانبي", "hide": "إخفاء الشريط الجانبي" }, @@ -5200,5 +5252,92 @@ "accept": "المشاركة بشكل مجهول", "decline": "لا، شكرًا" } + }, + "SecureFiles": { + "title": "الملفات", + "subtitle": "ملفات تبقى خاصة إلى أن تفتح قفلها — مشفّرة على جهازك، ومُقنَّعة على القرص", + "desktopOnly": "‏Secure Files متاح في تطبيق سطح المكتب.", + "allFiles": "كل الملفات", + "overview": "نظرة عامة", + "statFiles": "الملفات", + "statFolders": "المجلدات", + "statContent": "حجم المحتوى", + "statContentHint": "الحجم الإجمالي للملفات الأصلية", + "statOnDisk": "على القرص", + "statOverhead": "‏{size} زيادة بسبب التشفير", + "byType": "حسب النوع", + "fileCount": "{count, plural, zero {لا ملفات} one {ملف واحد} two {ملفان} few {# ملفات} many {# ملفًا} other {# ملف}}", + "largestFiles": "أكبر الملفات", + "recentlyAdded": "المضافة حديثًا", + "openContainingFolder": "فتح المجلد الذي يحتويه", + "types": { + "image": "الصور", + "video": "الفيديو", + "audio": "الصوت", + "pdf": "PDF", + "archive": "الأرشيفات", + "code": "الشيفرة", + "doc": "المستندات", + "sheet": "جداول البيانات", + "file": "أخرى" + }, + "breadcrumb": "مسار المجلد", + "addFiles": "إضافة ملفات", + "addFolder": "إضافة مجلد", + "importFolder": "استيراد مجلد", + "newFolder": "مجلد جديد", + "lock": "قفل", + "settings": "إعدادات التخزين", + "storageFolder": "مجلد التخزين", + "notSet": "غير محدد", + "chooseFolder": "اختيار مجلد", + "changeFolder": "تغيير المجلد…", + "chooseFolderTitle": "اختر مكان تخزين الملفات المشفّرة", + "chooseFolderBody": "اختر أي مجلد على هذا الجهاز أو على قرص خارجي. خارج MyDevTools لا يحتوي إلا على ملفات ‎.mydt‎ مُقنَّعة.", + "changeFolderTitle": "نقل الملفات المشفّرة؟", + "changeFolderBody": "ستُنقل جميع ملفات ‎.mydt‎ من مجلد التخزين الحالي إلى المجلد الذي ستختاره الآن.", + "folderSet": "تم تحديد مجلد التخزين", + "folderMissingTitle": "تعذّر العثور على مجلد التخزين", + "folderMissingBody": "‏«{dir}» غير متاح — قد يكون على قرص غير موصول أو يعود إلى نسخة احتياطية مستعادة من جهاز آخر. أعد توصيله أو اختر مجلدًا جديدًا.", + "movedCount": "تم نقل {count} من الملفات المشفّرة", + "importedCount": "تم تشفير {count} من الملفات", + "emptyFolderAdded": "تمت إضافة المجلد — لم يحتوِ على ملفات لتشفيرها", + "emptyTitle": "لا توجد ملفات هنا بعد", + "emptyBody": "أضف ملفات أو مجلدًا كاملًا. يُشفَّر كل ملف إلى كائن ‎.mydt‎ باسم عشوائي؛ الاسم الأصلي والنوع والمحتوى لا تظهر إلا هنا.", + "name": "الاسم", + "size": "الحجم", + "modified": "آخر تعديل", + "actions": "الإجراءات", + "expand": "توسيع", + "collapse": "طي", + "preview": "معاينة", + "noPreview": "لا تتوفر معاينة لهذا النوع من الملفات — صدّره لفتحه في تطبيق آخر.", + "export": "تصدير", + "exportWarningTitle": "تصدير نسخة غير مشفّرة؟", + "exportWarningBody": "سيُكتب الملف بدون تشفير في المكان الذي تختاره. يمكن لأي شيء على ذلك الجهاز قراءته.", + "exported": "تم تصدير النسخة غير المشفّرة", + "replace": "استبدال المحتوى…", + "replaced": "تم استبدال محتوى الملف", + "rename": "إعادة تسمية", + "fileName": "اسم الملف", + "move": "نقل إلى مجلد", + "targetFolder": "المجلد", + "targetFolderHint": "استخدم الشرطة المائلة للمجلدات المتداخلة، مثل project/config. اتركه فارغًا للمستوى الأعلى.", + "folderName": "اسم المجلد", + "delete": "حذف", + "deleted": "تم حذف الملف", + "deletedCount": "تم حذف {count} من الملفات", + "deleteFileTitle": "حذف هذا الملف؟", + "deleteFileBody": "سيُحذف «{name}» نهائيًا من مجلد التخزين المشفّر.", + "deleteFolderTitle": "حذف هذا المجلد؟", + "deleteFolderBody": "ستُحذف كل الملفات داخل «{dir}» نهائيًا من مجلد التخزين المشفّر.", + "unreadableCount": "ملفات غير قابلة للقراءة في مجلد التخزين: {count}", + "unreadableHint": "شُفِّرت بكلمة مرور رئيسية أخرى، أو تنتمي إلى خزنة أخرى، أو أنها تالفة.", + "dismiss": "إخفاء", + "viewGrid": "عرض شبكي", + "viewList": "عرض قائمة", + "footerNote": "ملفات حتى 20 ميغابايت. مشفّرة بـ Argon2id + XChaCha20-Poly1305؛ يبقى المفتاح في الذاكرة فقط ما دامت الخزنة مفتوحة.", + "cancel": "إلغاء", + "save": "حفظ" } } diff --git a/apps/desktop-ui/messages/ca.json b/apps/desktop-ui/messages/ca.json index edd94bda..0bbbc2fb 100644 --- a/apps/desktop-ui/messages/ca.json +++ b/apps/desktop-ui/messages/ca.json @@ -76,7 +76,8 @@ "tokenCounter": "Comptador de tokens", "webhookTester": "Provador de webhooks", "websocketTester": "Provador de WebSocket", - "whoisLookup": "Consulta Whois" + "whoisLookup": "Consulta Whois", + "secureFiles": "Fitxers" }, "Help": { "title": "Ajuda i documentació", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Edita", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "S'està restaurant…", "restoreSuccess": "Còpia de seguretat restaurada", "restoreError": "La restauració ha fallat — contrasenya incorrecta o fitxer de còpia no vàlid" + }, + "backupCodes": { + "title": "Codis de recuperació", + "description": "Codis d'un sol ús que recuperen la teva caixa forta si oblides la contrasenya mestra.", + "remaining": "Queden {remaining} de {total} codis", + "none": "No hi ha cap codi de recuperació desat.", + "hint": "Generar un conjunt nou invalida tots els codis existents.", + "passwordPlaceholder": "Contrasenya mestra", + "regenerateButton": "Genera codis nous", + "generating": "S'estan generant…", + "wrongPassword": "Contrasenya mestra incorrecta", + "success": "S'han generat codis nous", + "error": "No s'han pogut generar els codis", + "newCodesWarning": "No els podràs tornar a veure. Descarrega'ls o copia'ls ara.", + "downloadButton": "Baixa", + "doneButton": "Ja he desat els codis" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Consulta Whois", "description": "Consulta les dades de registre de dominis i IP mitjançant RDAP." + }, + "secureFiles": { + "title": "Fitxers", + "description": "Xifra fitxers i carpetes en objectes .mydt emmascarats; navega-hi i previsualitza'ls després de desbloquejar." } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "S'han mostrejat {docs} documents · {fields} camps", "colField": "Camp", "colTypes": "Tipus", - "colCoverage": "Cobertura" + "colCoverage": "Cobertura", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "Resposta copiada al porta-retalls", "codeCopied": "Codi copiat al porta-retalls", "copyFailed": "No s'ha pogut copiar al porta-retalls", - "curlCopied": "Ordre cURL copiada" + "curlCopied": "Ordre cURL copiada", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Col·leccions", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "Recursos API", "newCollection": "Col·lecció nova", + "openFolderCollection": "Obre una carpeta com a col·lecció", + "folderCollection": "Col·lecció de carpeta", "tabCollections": "Col·leccions", "tabHistory": "Historial", "noCollectionsTitle": "Sense col·leccions", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "Entorn nou", "variablesHeading": "Variables de {name}", "addVariable": "Afegeix variable", + "exportPostman": "Exporta (entorn de Postman)", "columnVariable": "Variable", "columnValue": "Valor", "placeholderKey": "Clau", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100% fora de línia" }, "ToolSidebar": { + "expand": "Amplia la barra lateral", + "collapse": "Redueix la barra lateral", + "resize": "Redimensiona la barra lateral", + "resetWidth": "Restableix l'amplada de la barra lateral", + "moreSections": "Més seccions", + "sections": "Seccions de la barra lateral", "show": "Mostra la barra lateral", "hide": "Amaga la barra lateral" }, @@ -5200,5 +5252,92 @@ "accept": "Comparteix anònimament", "decline": "No, gràcies" } + }, + "SecureFiles": { + "title": "Fitxers", + "subtitle": "Fitxers que continuen privats fins que els desbloqueges — xifrats al teu dispositiu, emmascarats al disc", + "desktopOnly": "Secure Files està disponible a l'aplicació d'escriptori.", + "allFiles": "Tots els fitxers", + "overview": "Resum", + "statFiles": "Fitxers", + "statFolders": "Carpetes", + "statContent": "Mida del contingut", + "statContentHint": "Mida total dels fitxers originals", + "statOnDisk": "Al disc", + "statOverhead": "{size} de sobrecàrrega de xifratge", + "byType": "Per tipus", + "fileCount": "{count, plural, one {# fitxer} other {# fitxers}}", + "largestFiles": "Fitxers més grans", + "recentlyAdded": "Afegits recentment", + "openContainingFolder": "Obre la carpeta que el conté", + "types": { + "image": "Imatges", + "video": "Vídeo", + "audio": "Àudio", + "pdf": "PDF", + "archive": "Arxius", + "code": "Codi", + "doc": "Documents", + "sheet": "Fulls de càlcul", + "file": "Altres" + }, + "breadcrumb": "Camí de la carpeta", + "addFiles": "Afegeix fitxers", + "addFolder": "Afegeix una carpeta", + "importFolder": "Importa una carpeta", + "newFolder": "Carpeta nova", + "lock": "Bloqueja", + "settings": "Configuració d'emmagatzematge", + "storageFolder": "Carpeta d'emmagatzematge", + "notSet": "Sense definir", + "chooseFolder": "Tria una carpeta", + "changeFolder": "Canvia de carpeta…", + "chooseFolderTitle": "Tria on es desen els fitxers xifrats", + "chooseFolderBody": "Tria qualsevol carpeta d'aquest dispositiu o d'una unitat externa. Fora de MyDevTools només conté fitxers .mydt emmascarats.", + "changeFolderTitle": "Voleu moure els fitxers xifrats?", + "changeFolderBody": "Tots els fitxers .mydt es mouran de la carpeta d'emmagatzematge actual a la que triïs tot seguit.", + "folderSet": "Carpeta d'emmagatzematge definida", + "folderMissingTitle": "No s'ha trobat la carpeta d'emmagatzematge", + "folderMissingBody": "«{dir}» no està disponible: pot ser en una unitat desconnectada o provenir d'una còpia de seguretat restaurada d'un altre ordinador. Torna-la a connectar o tria una carpeta nova.", + "movedCount": "S'han mogut {count} fitxers xifrats", + "importedCount": "S'han xifrat {count} fitxers", + "emptyFolderAdded": "Carpeta afegida: no contenia cap fitxer per xifrar", + "emptyTitle": "Encara no hi ha cap fitxer", + "emptyBody": "Afegeix fitxers o una carpeta sencera. Cada fitxer es xifra en un objecte .mydt amb un nom aleatori; el nom, el tipus i el contingut originals només es veuen aquí.", + "name": "Nom", + "size": "Mida", + "modified": "Modificat", + "actions": "Accions", + "expand": "Desplega", + "collapse": "Replega", + "preview": "Previsualització", + "noPreview": "No hi ha previsualització per a aquest tipus de fitxer: exporta'l per obrir-lo en una altra aplicació.", + "export": "Exporta", + "exportWarningTitle": "Voleu exportar una còpia desxifrada?", + "exportWarningBody": "El fitxer s'escriurà sense xifrar a la ubicació que triïs. Qualsevol cosa d'aquest dispositiu el podrà llegir.", + "exported": "Còpia desxifrada exportada", + "replace": "Substitueix el contingut…", + "replaced": "S'ha substituït el contingut del fitxer", + "rename": "Canvia el nom", + "fileName": "Nom del fitxer", + "move": "Mou a una carpeta", + "targetFolder": "Carpeta", + "targetFolderHint": "Fes servir barres per a carpetes imbricades, per exemple projecte/config. Deixa-ho buit per al nivell superior.", + "folderName": "Nom de la carpeta", + "delete": "Elimina", + "deleted": "Fitxer eliminat", + "deletedCount": "S'han eliminat {count} fitxers", + "deleteFileTitle": "Voleu eliminar aquest fitxer?", + "deleteFileBody": "«{name}» s'eliminarà permanentment de la carpeta d'emmagatzematge xifrada.", + "deleteFolderTitle": "Voleu eliminar aquesta carpeta?", + "deleteFolderBody": "Tots els fitxers dins de «{dir}» s'eliminaran permanentment de la carpeta d'emmagatzematge xifrada.", + "unreadableCount": "Fitxers illegibles a la carpeta d'emmagatzematge: {count}", + "unreadableHint": "Es van xifrar amb una altra contrasenya mestra, pertanyen a una altra caixa forta o estan malmesos.", + "dismiss": "Descarta", + "viewGrid": "Vista de quadrícula", + "viewList": "Vista de llista", + "footerNote": "Fitxers de fins a 20 MB. Xifrats amb Argon2id + XChaCha20-Poly1305; la clau només és a la memòria mentre la caixa forta està desbloquejada.", + "cancel": "Cancel·la", + "save": "Desa" } } diff --git a/apps/desktop-ui/messages/cs.json b/apps/desktop-ui/messages/cs.json index 1cecc88f..ae84c5af 100644 --- a/apps/desktop-ui/messages/cs.json +++ b/apps/desktop-ui/messages/cs.json @@ -76,7 +76,8 @@ "tokenCounter": "Počítadlo tokenů", "webhookTester": "Tester webhooků", "websocketTester": "Tester WebSocket", - "whoisLookup": "Whois vyhledávání" + "whoisLookup": "Whois vyhledávání", + "secureFiles": "Soubory" }, "Help": { "title": "Nápověda a dokumentace", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Upravit", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Obnovování…", "restoreSuccess": "Záloha obnovena", "restoreError": "Obnovení se nezdařilo — špatné heslo nebo neplatný soubor zálohy" + }, + "backupCodes": { + "title": "Záložní kódy", + "description": "Jednorázové kódy, které obnoví přístup k trezoru, když zapomenete hlavní heslo.", + "remaining": "Zbývá {remaining} z {total} kódů", + "none": "Nejsou uloženy žádné záložní kódy.", + "hint": "Vygenerování nové sady zneplatní všechny stávající kódy.", + "passwordPlaceholder": "Hlavní heslo", + "regenerateButton": "Vygenerovat nové kódy", + "generating": "Generuji…", + "wrongPassword": "Nesprávné hlavní heslo", + "success": "Nové záložní kódy byly vygenerovány", + "error": "Záložní kódy se nepodařilo vygenerovat", + "newCodesWarning": "Znovu je již neuvidíte. Stáhněte si je nebo je zkopírujte.", + "downloadButton": "Stáhnout", + "doneButton": "Kódy jsem uložil" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Whois vyhledávání", "description": "Vyhledejte registrační údaje domény a IP adresy přes RDAP." + }, + "secureFiles": { + "title": "Soubory", + "description": "Šifrujte soubory a složky do maskovaných objektů .mydt; po odemčení je můžete procházet a zobrazovat." } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "Vzorkováno {docs} dokumentů · {fields} polí", "colField": "Pole", "colTypes": "Typy", - "colCoverage": "Pokrytí" + "colCoverage": "Pokrytí", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "Odpověď zkopírována", "codeCopied": "Kód zkopírován", "copyFailed": "Nepodařilo se zkopírovat do schránky", - "curlCopied": "Příkaz cURL zkopírován" + "curlCopied": "Příkaz cURL zkopírován", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Kolekce", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "API zdroje", "newCollection": "Nová kolekce", + "openFolderCollection": "Otevřít složku jako kolekci", + "folderCollection": "Kolekce ze složky", "tabCollections": "Kolekce", "tabHistory": "Historie", "noCollectionsTitle": "Žádné kolekce", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "Nové prostředí", "variablesHeading": "Proměnné: {name}", "addVariable": "Přidat proměnnou", + "exportPostman": "Exportovat (prostředí Postman)", "columnVariable": "Proměnná", "columnValue": "Hodnota", "placeholderKey": "Klíč", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100% offline" }, "ToolSidebar": { + "expand": "Rozbalit postranní panel", + "collapse": "Sbalit postranní panel", + "resize": "Změnit šířku panelu", + "resetWidth": "Obnovit šířku panelu", + "moreSections": "Další sekce", + "sections": "Sekce postranního panelu", "show": "Zobrazit postranní panel", "hide": "Skrýt postranní panel" }, @@ -5200,5 +5252,92 @@ "accept": "Sdílet anonymně", "decline": "Ne, děkuji" } + }, + "SecureFiles": { + "title": "Soubory", + "subtitle": "Soubory zůstanou soukromé, dokud je neodemknete — zašifrované ve vašem zařízení, maskované na disku", + "desktopOnly": "Secure Files je k dispozici v desktopové aplikaci.", + "allFiles": "Všechny soubory", + "overview": "Přehled", + "statFiles": "Soubory", + "statFolders": "Složky", + "statContent": "Velikost obsahu", + "statContentHint": "Celková velikost původních souborů", + "statOnDisk": "Na disku", + "statOverhead": "{size} režie šifrování", + "byType": "Podle typu", + "fileCount": "{count, plural, one {# soubor} few {# soubory} many {# souboru} other {# souborů}}", + "largestFiles": "Největší soubory", + "recentlyAdded": "Nedávno přidané", + "openContainingFolder": "Otevřít nadřazenou složku", + "types": { + "image": "Obrázky", + "video": "Video", + "audio": "Zvuk", + "pdf": "PDF", + "archive": "Archivy", + "code": "Kód", + "doc": "Dokumenty", + "sheet": "Tabulky", + "file": "Ostatní" + }, + "breadcrumb": "Cesta ke složce", + "addFiles": "Přidat soubory", + "addFolder": "Přidat složku", + "importFolder": "Importovat složku", + "newFolder": "Nová složka", + "lock": "Zamknout", + "settings": "Nastavení úložiště", + "storageFolder": "Složka úložiště", + "notSet": "Nenastaveno", + "chooseFolder": "Vybrat složku", + "changeFolder": "Změnit složku…", + "chooseFolderTitle": "Vyberte, kam se ukládají zašifrované soubory", + "chooseFolderBody": "Vyberte libovolnou složku v tomto zařízení nebo na externím disku. Mimo MyDevTools obsahuje pouze maskované soubory .mydt.", + "changeFolderTitle": "Přesunout zašifrované soubory?", + "changeFolderBody": "Všechny soubory .mydt budou přesunuty ze současné složky úložiště do té, kterou vyberete nyní.", + "folderSet": "Složka úložiště nastavena", + "folderMissingTitle": "Složka úložiště nenalezena", + "folderMissingBody": "„{dir}“ není dostupná — může být na odpojeném disku nebo pocházet ze zálohy obnovené z jiného počítače. Připojte ji znovu, nebo vyberte novou složku.", + "movedCount": "Přesunuto zašifrovaných souborů: {count}", + "importedCount": "Zašifrováno souborů: {count}", + "emptyFolderAdded": "Složka přidána — neobsahovala žádné soubory k zašifrování", + "emptyTitle": "Zatím zde nejsou žádné soubory", + "emptyBody": "Přidejte soubory nebo celou složku. Každý soubor se zašifruje do objektu .mydt s náhodným názvem; původní název, typ a obsah jsou vidět jen zde.", + "name": "Název", + "size": "Velikost", + "modified": "Změněno", + "actions": "Akce", + "expand": "Rozbalit", + "collapse": "Sbalit", + "preview": "Náhled", + "noPreview": "Pro tento typ souboru není náhled k dispozici — exportujte jej a otevřete v jiné aplikaci.", + "export": "Exportovat", + "exportWarningTitle": "Exportovat dešifrovanou kopii?", + "exportWarningBody": "Soubor se zapíše nezašifrovaný do zvoleného umístění. Přečte si jej cokoli v tomto zařízení.", + "exported": "Dešifrovaná kopie exportována", + "replace": "Nahradit obsah…", + "replaced": "Obsah souboru nahrazen", + "rename": "Přejmenovat", + "fileName": "Název souboru", + "move": "Přesunout do složky", + "targetFolder": "Složka", + "targetFolderHint": "Pro vnořené složky použijte lomítka, například projekt/config. Pro nejvyšší úroveň nechte prázdné.", + "folderName": "Název složky", + "delete": "Smazat", + "deleted": "Soubor smazán", + "deletedCount": "Smazáno souborů: {count}", + "deleteFileTitle": "Smazat tento soubor?", + "deleteFileBody": "„{name}“ bude trvale odstraněn ze zašifrované složky úložiště.", + "deleteFolderTitle": "Smazat tuto složku?", + "deleteFolderBody": "Každý soubor ve složce „{dir}“ bude trvale odstraněn ze zašifrované složky úložiště.", + "unreadableCount": "Nečitelné soubory ve složce úložiště: {count}", + "unreadableHint": "Byly zašifrovány jiným hlavním heslem, patří do jiného trezoru nebo jsou poškozené.", + "dismiss": "Zavřít", + "viewGrid": "Zobrazení mřížky", + "viewList": "Zobrazení seznamu", + "footerNote": "Soubory do 20 MB. Šifrováno pomocí Argon2id + XChaCha20-Poly1305; klíč je v paměti jen po dobu odemčení trezoru.", + "cancel": "Zrušit", + "save": "Uložit" } } diff --git a/apps/desktop-ui/messages/da.json b/apps/desktop-ui/messages/da.json index c3b78cb8..c348e15d 100644 --- a/apps/desktop-ui/messages/da.json +++ b/apps/desktop-ui/messages/da.json @@ -76,7 +76,8 @@ "tokenCounter": "Token-tæller", "webhookTester": "Webhook-tester", "websocketTester": "WebSocket-tester", - "whoisLookup": "Whois-opslag" + "whoisLookup": "Whois-opslag", + "secureFiles": "Filer" }, "Help": { "title": "Hjælp og dokumentation", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Rediger", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Gendanner…", "restoreSuccess": "Sikkerhedskopi gendannet", "restoreError": "Gendannelse mislykkedes — forkert adgangssætning eller ugyldig fil" + }, + "backupCodes": { + "title": "Sikkerhedskoder", + "description": "Engangskoder, der gendanner din boks, hvis du glemmer din hovedadgangskode.", + "remaining": "{remaining} af {total} koder tilbage", + "none": "Ingen sikkerhedskoder gemt.", + "hint": "Nye koder ugyldiggør alle eksisterende koder.", + "passwordPlaceholder": "Hovedadgangskode", + "regenerateButton": "Generer nye koder", + "generating": "Genererer…", + "wrongPassword": "Forkert hovedadgangskode", + "success": "Nye sikkerhedskoder genereret", + "error": "Kunne ikke generere sikkerhedskoder", + "newCodesWarning": "Du kan ikke se dem igen. Download eller kopiér dem nu.", + "downloadButton": "Download", + "doneButton": "Jeg har gemt mine koder" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Whois-opslag", "description": "Slå registreringsoplysninger for domæner og IP-adresser op via RDAP." + }, + "secureFiles": { + "title": "Filer", + "description": "Krypter filer og mapper til maskerede .mydt-objekter; gennemse og forhåndsvis dem efter oplåsning." } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "Stikprøve af {docs} dokumenter · {fields} felter", "colField": "Felt", "colTypes": "Typer", - "colCoverage": "Dækning" + "colCoverage": "Dækning", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "Svar kopieret til udklipsholder", "codeCopied": "Kode kopieret til udklipsholder", "copyFailed": "Kopiering til udklipsholder mislykkedes", - "curlCopied": "cURL-kommando kopieret" + "curlCopied": "cURL-kommando kopieret", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Samlinger", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "API-ressourcer", "newCollection": "Ny kollektion", + "openFolderCollection": "Åbn mappe som samling", + "folderCollection": "Mappesamling", "tabCollections": "Samlinger", "tabHistory": "Historie", "noCollectionsTitle": "Ingen samlinger", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "Nyt miljø", "variablesHeading": "{name} variabler", "addVariable": "Tilføj variabel", + "exportPostman": "Eksportér (Postman-miljø)", "columnVariable": "Variabel", "columnValue": "Værdi", "placeholderKey": "Nøgle", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100 % offline" }, "ToolSidebar": { + "expand": "Udvid sidepanel", + "collapse": "Skjul sidepanel", + "resize": "Tilpas sidepanelets bredde", + "resetWidth": "Nulstil sidepanelets bredde", + "moreSections": "Flere sektioner", + "sections": "Sidepanelsektioner", "show": "Vis sidepanel", "hide": "Skjul sidepanel" }, @@ -5200,5 +5252,92 @@ "accept": "Del anonymt", "decline": "Nej tak" } + }, + "SecureFiles": { + "title": "Filer", + "subtitle": "Filer, der forbliver private, indtil du låser dem op — krypteret på din enhed, maskeret på disken", + "desktopOnly": "Secure Files er tilgængelig i skrivebordsappen.", + "allFiles": "Alle filer", + "overview": "Oversigt", + "statFiles": "Filer", + "statFolders": "Mapper", + "statContent": "Indholdsstørrelse", + "statContentHint": "Samlet størrelse af de oprindelige filer", + "statOnDisk": "På disken", + "statOverhead": "{size} krypteringsoverhead", + "byType": "Efter type", + "fileCount": "{count, plural, one {# fil} other {# filer}}", + "largestFiles": "Største filer", + "recentlyAdded": "Senest tilføjet", + "openContainingFolder": "Åbn overordnet mappe", + "types": { + "image": "Billeder", + "video": "Video", + "audio": "Lyd", + "pdf": "PDF", + "archive": "Arkiver", + "code": "Kode", + "doc": "Dokumenter", + "sheet": "Regneark", + "file": "Andet" + }, + "breadcrumb": "Mappesti", + "addFiles": "Tilføj filer", + "addFolder": "Tilføj mappe", + "importFolder": "Importér mappe", + "newFolder": "Ny mappe", + "lock": "Lås", + "settings": "Lagringsindstillinger", + "storageFolder": "Lagringsmappe", + "notSet": "Ikke angivet", + "chooseFolder": "Vælg mappe", + "changeFolder": "Skift mappe…", + "chooseFolderTitle": "Vælg, hvor krypterede filer gemmes", + "chooseFolderBody": "Vælg en vilkårlig mappe på denne enhed eller et eksternt drev. Uden for MyDevTools indeholder den kun maskerede .mydt-filer.", + "changeFolderTitle": "Flyt krypterede filer?", + "changeFolderBody": "Alle .mydt-filer flyttes fra den nuværende lagringsmappe til den, du vælger nu.", + "folderSet": "Lagringsmappe angivet", + "folderMissingTitle": "Lagringsmappen blev ikke fundet", + "folderMissingBody": "“{dir}” er ikke tilgængelig — den ligger måske på et frakoblet drev eller stammer fra en gendannet sikkerhedskopi fra en anden maskine. Tilslut den igen, eller vælg en ny mappe.", + "movedCount": "Flyttede {count} krypterede filer", + "importedCount": "Krypterede {count} filer", + "emptyFolderAdded": "Mappe tilføjet — den indeholdt ingen filer at kryptere", + "emptyTitle": "Ingen filer her endnu", + "emptyBody": "Tilføj filer eller en hel mappe. Hver fil krypteres til et .mydt-objekt med et tilfældigt navn; det oprindelige navn, typen og indholdet ses kun her.", + "name": "Navn", + "size": "Størrelse", + "modified": "Ændret", + "actions": "Handlinger", + "expand": "Udvid", + "collapse": "Skjul", + "preview": "Forhåndsvisning", + "noPreview": "Ingen forhåndsvisning af denne filtype — eksportér den for at åbne den i en anden app.", + "export": "Eksportér", + "exportWarningTitle": "Eksportér en dekrypteret kopi?", + "exportWarningBody": "Filen skrives ukrypteret til den placering, du vælger. Alt på den enhed kan læse den.", + "exported": "Dekrypteret kopi eksporteret", + "replace": "Erstat indhold…", + "replaced": "Filens indhold blev erstattet", + "rename": "Omdøb", + "fileName": "Filnavn", + "move": "Flyt til mappe", + "targetFolder": "Mappe", + "targetFolderHint": "Brug skråstreger til indlejrede mapper, for eksempel projekt/config. Lad feltet være tomt for øverste niveau.", + "folderName": "Mappenavn", + "delete": "Slet", + "deleted": "Filen blev slettet", + "deletedCount": "Slettede {count} filer", + "deleteFileTitle": "Slet denne fil?", + "deleteFileBody": "“{name}” fjernes permanent fra den krypterede lagringsmappe.", + "deleteFolderTitle": "Slet denne mappe?", + "deleteFolderBody": "Alle filer under “{dir}” fjernes permanent fra den krypterede lagringsmappe.", + "unreadableCount": "Ulæselige filer i lagringsmappen: {count}", + "unreadableHint": "De blev krypteret med en anden hovedadgangskode, hører til et andet boks-arkiv eller er beskadigede.", + "dismiss": "Afvis", + "viewGrid": "Gittervisning", + "viewList": "Listevisning", + "footerNote": "Filer op til 20 MB. Krypteret med Argon2id + XChaCha20-Poly1305; nøglen findes kun i hukommelsen, mens boksen er låst op.", + "cancel": "Annuller", + "save": "Gem" } } diff --git a/apps/desktop-ui/messages/de.json b/apps/desktop-ui/messages/de.json index 33813e79..b3af3829 100644 --- a/apps/desktop-ui/messages/de.json +++ b/apps/desktop-ui/messages/de.json @@ -76,7 +76,8 @@ "tokenCounter": "Token-Zähler", "webhookTester": "Webhook-Tester", "websocketTester": "WebSocket-Tester", - "whoisLookup": "Whois-Abfrage" + "whoisLookup": "Whois-Abfrage", + "secureFiles": "Dateien" }, "Help": { "title": "Hilfe & Dokumentation", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Bearbeiten", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Wird wiederhergestellt…", "restoreSuccess": "Backup wiederhergestellt", "restoreError": "Wiederherstellung fehlgeschlagen — falsche Passphrase oder ungültige Backup-Datei" + }, + "backupCodes": { + "title": "Backup-Codes", + "description": "Einmalcodes, mit denen Sie Ihren Tresor wiederherstellen, wenn Sie Ihr Master-Passwort vergessen.", + "remaining": "Noch {remaining} von {total} Codes", + "none": "Keine Backup-Codes gespeichert.", + "hint": "Ein neuer Satz macht alle bestehenden Codes ungültig.", + "passwordPlaceholder": "Master-Passwort", + "regenerateButton": "Neue Codes erzeugen", + "generating": "Wird erzeugt…", + "wrongPassword": "Falsches Master-Passwort", + "success": "Neue Backup-Codes erzeugt", + "error": "Backup-Codes konnten nicht erzeugt werden", + "newCodesWarning": "Sie können sie nicht erneut ansehen. Laden Sie sie jetzt herunter oder kopieren Sie sie.", + "downloadButton": "Herunterladen", + "doneButton": "Ich habe meine Codes gespeichert" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Whois-Abfrage", "description": "Registrierungsdaten von Domains und IPs per RDAP abrufen." + }, + "secureFiles": { + "title": "Dateien", + "description": "Verschlüsseln Sie Dateien und Ordner in maskierte .mydt-Objekte; nach dem Entsperren durchsuchen und ansehen." } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "{docs} Dokumente untersucht · {fields} Felder", "colField": "Feld", "colTypes": "Typen", - "colCoverage": "Abdeckung" + "colCoverage": "Abdeckung", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "Antwort in die Zwischenablage kopiert", "codeCopied": "Code in die Zwischenablage kopiert", "copyFailed": "In die Zwischenablage kopieren fehlgeschlagen", - "curlCopied": "cURL-Befehl kopiert" + "curlCopied": "cURL-Befehl kopiert", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Sammlungen", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "API-Ressourcen", "newCollection": "Neue Sammlung", + "openFolderCollection": "Ordner als Sammlung öffnen", + "folderCollection": "Ordner-Sammlung", "tabCollections": "Sammlungen", "tabHistory": "Verlauf", "noCollectionsTitle": "Keine Sammlungen", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "Neue Umgebung", "variablesHeading": "Variablen: {name}", "addVariable": "Variable hinzufügen", + "exportPostman": "Exportieren (Postman-Umgebung)", "columnVariable": "Variable", "columnValue": "Wert", "placeholderKey": "Schlüssel", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100 % offline" }, "ToolSidebar": { + "expand": "Seitenleiste ausklappen", + "collapse": "Seitenleiste einklappen", + "resize": "Seitenleiste anpassen", + "resetWidth": "Breite zurücksetzen", + "moreSections": "Weitere Abschnitte", + "sections": "Abschnitte der Seitenleiste", "show": "Seitenleiste einblenden", "hide": "Seitenleiste ausblenden" }, @@ -5200,5 +5252,92 @@ "accept": "Anonym teilen", "decline": "Nein, danke" } + }, + "SecureFiles": { + "title": "Dateien", + "subtitle": "Dateien, die privat bleiben, bis Sie sie entsperren — auf Ihrem Gerät verschlüsselt, auf der Festplatte maskiert", + "desktopOnly": "Secure Files ist in der Desktop-App verfügbar.", + "allFiles": "Alle Dateien", + "overview": "Übersicht", + "statFiles": "Dateien", + "statFolders": "Ordner", + "statContent": "Inhaltsgröße", + "statContentHint": "Gesamtgröße der Originaldateien", + "statOnDisk": "Auf der Festplatte", + "statOverhead": "{size} Verschlüsselungs-Overhead", + "byType": "Nach Typ", + "fileCount": "{count, plural, one {# Datei} other {# Dateien}}", + "largestFiles": "Größte Dateien", + "recentlyAdded": "Zuletzt hinzugefügt", + "openContainingFolder": "Übergeordneten Ordner öffnen", + "types": { + "image": "Bilder", + "video": "Video", + "audio": "Audio", + "pdf": "PDF", + "archive": "Archive", + "code": "Code", + "doc": "Dokumente", + "sheet": "Tabellen", + "file": "Sonstige" + }, + "breadcrumb": "Ordnerpfad", + "addFiles": "Dateien hinzufügen", + "addFolder": "Ordner hinzufügen", + "importFolder": "Ordner importieren", + "newFolder": "Neuer Ordner", + "lock": "Sperren", + "settings": "Speichereinstellungen", + "storageFolder": "Speicherordner", + "notSet": "Nicht festgelegt", + "chooseFolder": "Ordner wählen", + "changeFolder": "Ordner ändern…", + "chooseFolderTitle": "Wählen Sie, wo verschlüsselte Dateien gespeichert werden", + "chooseFolderBody": "Wählen Sie einen beliebigen Ordner auf diesem Gerät oder einem externen Laufwerk. Außerhalb von MyDevTools enthält er ausschließlich maskierte .mydt-Dateien.", + "changeFolderTitle": "Verschlüsselte Dateien verschieben?", + "changeFolderBody": "Alle .mydt-Dateien werden aus dem aktuellen Speicherordner in den als Nächstes gewählten verschoben.", + "folderSet": "Speicherordner festgelegt", + "folderMissingTitle": "Speicherordner nicht gefunden", + "folderMissingBody": "„{dir}“ ist nicht verfügbar — der Ordner liegt möglicherweise auf einem nicht angeschlossenen Laufwerk oder stammt aus einer wiederhergestellten Sicherung eines anderen Rechners. Schließen Sie ihn wieder an oder wählen Sie einen neuen Ordner.", + "movedCount": "{count} verschlüsselte Dateien verschoben", + "importedCount": "{count} Dateien verschlüsselt", + "emptyFolderAdded": "Ordner hinzugefügt — er enthielt keine Dateien zum Verschlüsseln", + "emptyTitle": "Hier sind noch keine Dateien", + "emptyBody": "Fügen Sie Dateien oder einen ganzen Ordner hinzu. Jede Datei wird in ein .mydt-Objekt mit zufälligem Namen verschlüsselt; Originalname, Typ und Inhalt sind nur hier sichtbar.", + "name": "Name", + "size": "Größe", + "modified": "Geändert", + "actions": "Aktionen", + "expand": "Ausklappen", + "collapse": "Einklappen", + "preview": "Vorschau", + "noPreview": "Keine Vorschau für diesen Dateityp — exportieren Sie die Datei, um sie in einer anderen App zu öffnen.", + "export": "Exportieren", + "exportWarningTitle": "Entschlüsselte Kopie exportieren?", + "exportWarningBody": "Die Datei wird unverschlüsselt an den gewählten Ort geschrieben. Alles auf diesem Gerät kann sie lesen.", + "exported": "Entschlüsselte Kopie exportiert", + "replace": "Inhalt ersetzen…", + "replaced": "Dateiinhalt ersetzt", + "rename": "Umbenennen", + "fileName": "Dateiname", + "move": "In Ordner verschieben", + "targetFolder": "Ordner", + "targetFolderHint": "Verwenden Sie Schrägstriche für verschachtelte Ordner, z. B. projekt/config. Für die oberste Ebene leer lassen.", + "folderName": "Ordnername", + "delete": "Löschen", + "deleted": "Datei gelöscht", + "deletedCount": "{count} Dateien gelöscht", + "deleteFileTitle": "Diese Datei löschen?", + "deleteFileBody": "„{name}“ wird endgültig aus dem verschlüsselten Speicherordner entfernt.", + "deleteFolderTitle": "Diesen Ordner löschen?", + "deleteFolderBody": "Jede Datei unter „{dir}“ wird endgültig aus dem verschlüsselten Speicherordner entfernt.", + "unreadableCount": "Nicht lesbare Dateien im Speicherordner: {count}", + "unreadableHint": "Sie wurden mit einem anderen Master-Passwort verschlüsselt, gehören zu einem anderen Tresor oder sind beschädigt.", + "dismiss": "Ausblenden", + "viewGrid": "Rasteransicht", + "viewList": "Listenansicht", + "footerNote": "Dateien bis 20 MB. Verschlüsselt mit Argon2id + XChaCha20-Poly1305; der Schlüssel bleibt nur im Arbeitsspeicher, solange der Tresor entsperrt ist.", + "cancel": "Abbrechen", + "save": "Speichern" } } diff --git a/apps/desktop-ui/messages/el.json b/apps/desktop-ui/messages/el.json index 98a744c5..d441276c 100644 --- a/apps/desktop-ui/messages/el.json +++ b/apps/desktop-ui/messages/el.json @@ -76,7 +76,8 @@ "tokenCounter": "Μετρητής token", "webhookTester": "Δοκιμαστής webhook", "websocketTester": "Δοκιμαστής WebSocket", - "whoisLookup": "Αναζήτηση Whois" + "whoisLookup": "Αναζήτηση Whois", + "secureFiles": "Αρχεία" }, "Help": { "title": "Βοήθεια και τεκμηρίωση", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Επεξεργασία", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Γίνεται επαναφορά…", "restoreSuccess": "Το αντίγραφο ασφαλείας επαναφέρθηκε", "restoreError": "Η επαναφορά απέτυχε — λάθος φράση πρόσβασης ή μη έγκυρο αρχείο" + }, + "backupCodes": { + "title": "Εφεδρικοί κωδικοί", + "description": "Κωδικοί μίας χρήσης που επαναφέρουν το θησαυροφυλάκιό σας αν ξεχάσετε τον κύριο κωδικό.", + "remaining": "Απομένουν {remaining} από {total} κωδικοί", + "none": "Δεν έχουν αποθηκευτεί εφεδρικοί κωδικοί.", + "hint": "Η δημιουργία νέων κωδικών ακυρώνει όλους τους υπάρχοντες.", + "passwordPlaceholder": "Κύριος κωδικός", + "regenerateButton": "Δημιουργία νέων κωδικών", + "generating": "Δημιουργία…", + "wrongPassword": "Λανθασμένος κύριος κωδικός", + "success": "Δημιουργήθηκαν νέοι εφεδρικοί κωδικοί", + "error": "Δεν ήταν δυνατή η δημιουργία κωδικών", + "newCodesWarning": "Δεν θα μπορείτε να τους δείτε ξανά. Κατεβάστε ή αντιγράψτε τους τώρα.", + "downloadButton": "Λήψη", + "doneButton": "Αποθήκευσα τους κωδικούς μου" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Αναζήτηση Whois", "description": "Αναζητήστε στοιχεία καταχώρισης τομέα και IP μέσω RDAP." + }, + "secureFiles": { + "title": "Αρχεία", + "description": "Κρυπτογραφήστε αρχεία και φακέλους σε μεταμφιεσμένα αντικείμενα .mydt· περιηγηθείτε και δείτε τα μετά το ξεκλείδωμα." } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "Δείγμα {docs} εγγράφων · {fields} πεδία", "colField": "Πεδίο", "colTypes": "Τύποι", - "colCoverage": "Κάλυψη" + "colCoverage": "Κάλυψη", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "Η απάντηση αντιγράφηκε στο πρόχειρο", "codeCopied": "Ο κώδικας αντιγράφηκε στο πρόχειρο", "copyFailed": "Αποτυχία αντιγραφής στο πρόχειρο", - "curlCopied": "Η εντολή cURL αντιγράφηκε" + "curlCopied": "Η εντολή cURL αντιγράφηκε", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Συλλογές", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "Πόροι API", "newCollection": "Νέα συλλογή", + "openFolderCollection": "Άνοιγμα φακέλου ως συλλογή", + "folderCollection": "Συλλογή φακέλου", "tabCollections": "Συλλογές", "tabHistory": "Ιστορία", "noCollectionsTitle": "Χωρίς συλλογές", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "Νέο περιβάλλον", "variablesHeading": "{name} μεταβλητές", "addVariable": "Προσθήκη μεταβλητής", + "exportPostman": "Εξαγωγή (περιβάλλον Postman)", "columnVariable": "Μεταβλητός", "columnValue": "Αξία", "placeholderKey": "Κλειδί", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100% εκτός σύνδεσης" }, "ToolSidebar": { + "expand": "Ανάπτυξη πλαϊνής στήλης", + "collapse": "Σύμπτυξη πλαϊνής στήλης", + "resize": "Αλλαγή πλάτους", + "resetWidth": "Επαναφορά πλάτους", + "moreSections": "Περισσότερες ενότητες", + "sections": "Ενότητες πλαϊνής στήλης", "show": "Εμφάνιση πλαϊνής μπάρας", "hide": "Απόκρυψη πλαϊνής μπάρας" }, @@ -5200,5 +5252,92 @@ "accept": "Ανώνυμη κοινοποίηση", "decline": "Όχι, ευχαριστώ" } + }, + "SecureFiles": { + "title": "Αρχεία", + "subtitle": "Αρχεία που παραμένουν ιδιωτικά μέχρι να τα ξεκλειδώσετε — κρυπτογραφημένα στη συσκευή σας, μεταμφιεσμένα στον δίσκο", + "desktopOnly": "Το Secure Files είναι διαθέσιμο στην εφαρμογή για υπολογιστή.", + "allFiles": "Όλα τα αρχεία", + "overview": "Επισκόπηση", + "statFiles": "Αρχεία", + "statFolders": "Φάκελοι", + "statContent": "Μέγεθος περιεχομένου", + "statContentHint": "Συνολικό μέγεθος των αρχικών αρχείων", + "statOnDisk": "Στον δίσκο", + "statOverhead": "{size} επιβάρυνση κρυπτογράφησης", + "byType": "Ανά τύπο", + "fileCount": "{count, plural, one {# αρχείο} other {# αρχεία}}", + "largestFiles": "Μεγαλύτερα αρχεία", + "recentlyAdded": "Προστέθηκαν πρόσφατα", + "openContainingFolder": "Άνοιγμα φακέλου που το περιέχει", + "types": { + "image": "Εικόνες", + "video": "Βίντεο", + "audio": "Ήχος", + "pdf": "PDF", + "archive": "Αρχειοθήκες", + "code": "Κώδικας", + "doc": "Έγγραφα", + "sheet": "Υπολογιστικά φύλλα", + "file": "Άλλα" + }, + "breadcrumb": "Διαδρομή φακέλου", + "addFiles": "Προσθήκη αρχείων", + "addFolder": "Προσθήκη φακέλου", + "importFolder": "Εισαγωγή φακέλου", + "newFolder": "Νέος φάκελος", + "lock": "Κλείδωμα", + "settings": "Ρυθμίσεις αποθήκευσης", + "storageFolder": "Φάκελος αποθήκευσης", + "notSet": "Δεν έχει οριστεί", + "chooseFolder": "Επιλογή φακέλου", + "changeFolder": "Αλλαγή φακέλου…", + "chooseFolderTitle": "Επιλέξτε πού αποθηκεύονται τα κρυπτογραφημένα αρχεία", + "chooseFolderBody": "Επιλέξτε οποιονδήποτε φάκελο σε αυτή τη συσκευή ή σε εξωτερικό δίσκο. Εκτός του MyDevTools περιέχει μόνο μεταμφιεσμένα αρχεία .mydt.", + "changeFolderTitle": "Μετακίνηση κρυπτογραφημένων αρχείων;", + "changeFolderBody": "Όλα τα αρχεία .mydt θα μετακινηθούν από τον τρέχοντα φάκελο αποθήκευσης σε αυτόν που θα επιλέξετε στη συνέχεια.", + "folderSet": "Ο φάκελος αποθήκευσης ορίστηκε", + "folderMissingTitle": "Ο φάκελος αποθήκευσης δεν βρέθηκε", + "folderMissingBody": "Το «{dir}» δεν είναι διαθέσιμο — ίσως βρίσκεται σε αποσυνδεδεμένο δίσκο ή προέρχεται από αντίγραφο ασφαλείας άλλου υπολογιστή. Συνδέστε το ξανά ή επιλέξτε νέο φάκελο.", + "movedCount": "Μετακινήθηκαν {count} κρυπτογραφημένα αρχεία", + "importedCount": "Κρυπτογραφήθηκαν {count} αρχεία", + "emptyFolderAdded": "Ο φάκελος προστέθηκε — δεν περιείχε αρχεία προς κρυπτογράφηση", + "emptyTitle": "Δεν υπάρχουν ακόμη αρχεία εδώ", + "emptyBody": "Προσθέστε αρχεία ή έναν ολόκληρο φάκελο. Κάθε αρχείο κρυπτογραφείται σε αντικείμενο .mydt με τυχαίο όνομα· το αρχικό όνομα, ο τύπος και το περιεχόμενο φαίνονται μόνο εδώ.", + "name": "Όνομα", + "size": "Μέγεθος", + "modified": "Τροποποιήθηκε", + "actions": "Ενέργειες", + "expand": "Ανάπτυξη", + "collapse": "Σύμπτυξη", + "preview": "Προεπισκόπηση", + "noPreview": "Δεν υπάρχει προεπισκόπηση για αυτόν τον τύπο αρχείου — εξαγάγετέ το για να το ανοίξετε σε άλλη εφαρμογή.", + "export": "Εξαγωγή", + "exportWarningTitle": "Εξαγωγή αποκρυπτογραφημένου αντιγράφου;", + "exportWarningBody": "Το αρχείο θα γραφτεί χωρίς κρυπτογράφηση στη θέση που θα επιλέξετε. Οτιδήποτε σε εκείνη τη συσκευή θα μπορεί να το διαβάσει.", + "exported": "Το αποκρυπτογραφημένο αντίγραφο εξήχθη", + "replace": "Αντικατάσταση περιεχομένου…", + "replaced": "Το περιεχόμενο του αρχείου αντικαταστάθηκε", + "rename": "Μετονομασία", + "fileName": "Όνομα αρχείου", + "move": "Μετακίνηση σε φάκελο", + "targetFolder": "Φάκελος", + "targetFolderHint": "Χρησιμοποιήστε καθέτους για ένθετους φακέλους, π.χ. project/config. Αφήστε το κενό για το ανώτατο επίπεδο.", + "folderName": "Όνομα φακέλου", + "delete": "Διαγραφή", + "deleted": "Το αρχείο διαγράφηκε", + "deletedCount": "Διαγράφηκαν {count} αρχεία", + "deleteFileTitle": "Διαγραφή αυτού του αρχείου;", + "deleteFileBody": "Το «{name}» θα αφαιρεθεί οριστικά από τον κρυπτογραφημένο φάκελο αποθήκευσης.", + "deleteFolderTitle": "Διαγραφή αυτού του φακέλου;", + "deleteFolderBody": "Κάθε αρχείο μέσα στο «{dir}» θα αφαιρεθεί οριστικά από τον κρυπτογραφημένο φάκελο αποθήκευσης.", + "unreadableCount": "Μη αναγνώσιμα αρχεία στον φάκελο αποθήκευσης: {count}", + "unreadableHint": "Κρυπτογραφήθηκαν με άλλον κύριο κωδικό, ανήκουν σε άλλο θησαυροφυλάκιο ή είναι κατεστραμμένα.", + "dismiss": "Απόρριψη", + "viewGrid": "Προβολή πλέγματος", + "viewList": "Προβολή λίστας", + "footerNote": "Αρχεία έως 20 MB. Κρυπτογράφηση με Argon2id + XChaCha20-Poly1305· το κλειδί παραμένει στη μνήμη μόνο όσο το θησαυροφυλάκιο είναι ξεκλείδωτο.", + "cancel": "Ακύρωση", + "save": "Αποθήκευση" } } diff --git a/apps/desktop-ui/messages/en.json b/apps/desktop-ui/messages/en.json index 24f9b8cd..a430419d 100644 --- a/apps/desktop-ui/messages/en.json +++ b/apps/desktop-ui/messages/en.json @@ -43,6 +43,7 @@ "gitignoreGenerator": ".gitignore Generator", "csvExcelJson": "CSV / Excel ↔ JSON", "snippetManager": "Code Snippets", + "secureFiles": "Files", "markdownPreview": "Markdown Preview", "numberBaseConverter": "Number Base Converter", "formatConverter": "Format Converter", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Edit", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Restoring…", "restoreSuccess": "Backup restored", "restoreError": "Restore failed — wrong passphrase or invalid backup file" + }, + "backupCodes": { + "title": "Backup codes", + "description": "One-time codes that recover your vault if you forget your master password.", + "remaining": "{remaining} of {total} codes left", + "none": "No backup codes stored.", + "hint": "Generating a new set invalidates every existing code.", + "passwordPlaceholder": "Master password", + "regenerateButton": "Generate new codes", + "generating": "Generating…", + "wrongPassword": "Incorrect master password", + "success": "New backup codes generated", + "error": "Could not generate backup codes", + "newCodesWarning": "You cannot view these again. Download or copy them now.", + "downloadButton": "Download", + "doneButton": "I've saved my codes" } }, "Dashboard": { @@ -626,6 +644,10 @@ "title": "Code Snippets", "description": "Save snippets with syntax highlighting, auto language detection, and formatting." }, + "secureFiles": { + "title": "Files", + "description": "Encrypt files and folders into masked .mydt objects; browse and preview them after unlocking." + }, "markdownPreview": { "title": "Markdown Preview", "description": "Live Markdown renderer with HTML export and HTML → Markdown conversion." @@ -2352,6 +2374,8 @@ "collectionsSidebar": { "title": "API resources", "newCollection": "New collection", + "openFolderCollection": "Open folder as collection", + "folderCollection": "Folder collection", "tabCollections": "Collections", "tabHistory": "History", "noCollectionsTitle": "No collections", @@ -2393,6 +2417,7 @@ "placeholderNewEnv": "New environment", "variablesHeading": "{name} variables", "addVariable": "Add variable", + "exportPostman": "Export (Postman environment)", "columnVariable": "Variable", "columnValue": "Value", "placeholderKey": "Key", @@ -3136,6 +3161,93 @@ "detectedFormats": "Detected formats", "download": "Download" }, + "SecureFiles": { + "title": "Files", + "subtitle": "Files that stay private until you unlock them — encrypted on your device, masked on disk", + "desktopOnly": "Secure Files is available in the desktop app.", + "allFiles": "All files", + "overview": "Overview", + "statFiles": "Files", + "statFolders": "Folders", + "statContent": "Content size", + "statContentHint": "Total size of the original files", + "statOnDisk": "On disk", + "statOverhead": "{size} encryption overhead", + "byType": "By type", + "fileCount": "{count, plural, one {# file} other {# files}}", + "largestFiles": "Largest files", + "recentlyAdded": "Recently added", + "openContainingFolder": "Open containing folder", + "types": { + "image": "Images", + "video": "Video", + "audio": "Audio", + "pdf": "PDF", + "archive": "Archives", + "code": "Code", + "doc": "Documents", + "sheet": "Spreadsheets", + "file": "Other" + }, + "breadcrumb": "Folder path", + "addFiles": "Add files", + "addFolder": "Add folder", + "importFolder": "Import folder", + "newFolder": "New folder", + "lock": "Lock", + "settings": "Storage settings", + "storageFolder": "Storage folder", + "notSet": "Not set", + "chooseFolder": "Choose folder", + "changeFolder": "Change folder…", + "chooseFolderTitle": "Choose where encrypted files are stored", + "chooseFolderBody": "Pick any folder on this device or an external drive. Outside MyDevTools it only ever contains masked .mydt files.", + "changeFolderTitle": "Move encrypted files?", + "changeFolderBody": "All .mydt files will be moved from the current storage folder into the one you pick next.", + "folderSet": "Storage folder set", + "folderMissingTitle": "Storage folder not found", + "folderMissingBody": "\"{dir}\" is not available — it may be on an unplugged drive or belong to a restored backup from another machine. Reconnect it, or choose a new folder.", + "movedCount": "Moved {count} encrypted files", + "importedCount": "Encrypted {count} files", + "emptyFolderAdded": "Folder added — it had no files to encrypt", + "emptyTitle": "No files here yet", + "emptyBody": "Add files or a whole folder. Each file is encrypted into a .mydt object with a random name; the original name, type and contents are only visible here.", + "name": "Name", + "size": "Size", + "modified": "Modified", + "actions": "Actions", + "expand": "Expand", + "collapse": "Collapse", + "preview": "Preview", + "noPreview": "No preview for this file type — export it to open it in another app.", + "export": "Export", + "exportWarningTitle": "Export a decrypted copy?", + "exportWarningBody": "The file will be written unencrypted to the location you choose. Anything on that device can read it.", + "exported": "Decrypted copy exported", + "replace": "Replace contents…", + "replaced": "File contents replaced", + "rename": "Rename", + "fileName": "File name", + "move": "Move to folder", + "targetFolder": "Folder", + "targetFolderHint": "Use slashes for nested folders, e.g. project/config. Leave empty for the top level.", + "folderName": "Folder name", + "delete": "Delete", + "deleted": "File deleted", + "deletedCount": "Deleted {count} files", + "deleteFileTitle": "Delete this file?", + "deleteFileBody": "\"{name}\" will be permanently removed from the encrypted storage folder.", + "deleteFolderTitle": "Delete this folder?", + "deleteFolderBody": "Every file under \"{dir}\" will be permanently removed from the encrypted storage folder.", + "unreadableCount": "Unreadable files in the storage folder: {count}", + "unreadableHint": "They were encrypted with a different master password, belong to another vault, or are corrupted.", + "dismiss": "Dismiss", + "viewGrid": "Grid view", + "viewList": "List view", + "footerNote": "Files up to 20 MB. Encrypted with Argon2id + XChaCha20-Poly1305; the key lives in memory only while the vault is unlocked.", + "cancel": "Cancel", + "save": "Save" + }, "SnippetManager": { "title": "Snippet Manager", "subtitle": "Save, search, and format code snippets — sync optional", @@ -5198,6 +5310,12 @@ "offline": "100% offline" }, "ToolSidebar": { + "expand": "Expand sidebar", + "collapse": "Collapse sidebar", + "resize": "Resize sidebar", + "resetWidth": "Reset sidebar width", + "moreSections": "More sections", + "sections": "Sidebar sections", "show": "Show sidebar", "hide": "Hide sidebar" }, diff --git a/apps/desktop-ui/messages/es.json b/apps/desktop-ui/messages/es.json index ddd9d36a..b5bd3aa9 100644 --- a/apps/desktop-ui/messages/es.json +++ b/apps/desktop-ui/messages/es.json @@ -76,7 +76,8 @@ "tokenCounter": "Contador de tokens", "webhookTester": "Probador de webhooks", "websocketTester": "Probador de WebSocket", - "whoisLookup": "Consulta Whois" + "whoisLookup": "Consulta Whois", + "secureFiles": "Archivos" }, "Help": { "title": "Ayuda y documentación", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Editar", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Restaurando…", "restoreSuccess": "Copia de seguridad restaurada", "restoreError": "La restauración falló — frase incorrecta o archivo de copia no válido" + }, + "backupCodes": { + "title": "Códigos de respaldo", + "description": "Códigos de un solo uso que recuperan tu bóveda si olvidas la contraseña maestra.", + "remaining": "Quedan {remaining} de {total} códigos", + "none": "No hay códigos de respaldo guardados.", + "hint": "Generar un conjunto nuevo invalida todos los códigos existentes.", + "passwordPlaceholder": "Contraseña maestra", + "regenerateButton": "Generar códigos nuevos", + "generating": "Generando…", + "wrongPassword": "Contraseña maestra incorrecta", + "success": "Nuevos códigos de respaldo generados", + "error": "No se pudieron generar los códigos de respaldo", + "newCodesWarning": "No podrás volver a verlos. Descárgalos o cópialos ahora.", + "downloadButton": "Descargar", + "doneButton": "Ya guardé mis códigos" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Consulta Whois", "description": "Consulta los datos de registro de un dominio o IP mediante RDAP." + }, + "secureFiles": { + "title": "Archivos", + "description": "Cifra archivos y carpetas en objetos .mydt enmascarados; explóralos y previsualízalos tras desbloquear." } }, "tabs": { @@ -2278,7 +2300,8 @@ "responseCopied": "Respuesta copiada al portapapeles", "codeCopied": "Código copiado al portapapeles", "copyFailed": "Error al copiar al portapapeles", - "curlCopied": "Comando cURL copiado" + "curlCopied": "Comando cURL copiado", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Colecciones", @@ -2351,6 +2374,8 @@ "collectionsSidebar": { "title": "Recursos API", "newCollection": "Nueva colección", + "openFolderCollection": "Abrir carpeta como colección", + "folderCollection": "Colección de carpeta", "tabCollections": "Colecciones", "tabHistory": "Historial", "noCollectionsTitle": "Sin colecciones", @@ -2392,6 +2417,7 @@ "placeholderNewEnv": "Nuevo entorno", "variablesHeading": "Variables de {name}", "addVariable": "Añadir variable", + "exportPostman": "Exportar (entorno de Postman)", "columnVariable": "Variable", "columnValue": "Valor", "placeholderKey": "Clave", @@ -2448,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5188,6 +5223,12 @@ "offline": "100% sin conexión" }, "ToolSidebar": { + "expand": "Expandir barra lateral", + "collapse": "Contraer barra lateral", + "resize": "Redimensionar barra lateral", + "resetWidth": "Restablecer ancho", + "moreSections": "Más secciones", + "sections": "Secciones de la barra lateral", "show": "Mostrar barra lateral", "hide": "Ocultar barra lateral" }, @@ -5211,5 +5252,92 @@ "accept": "Compartir de forma anónima", "decline": "No, gracias" } + }, + "SecureFiles": { + "title": "Archivos", + "subtitle": "Archivos que siguen siendo privados hasta que los desbloqueas: cifrados en tu dispositivo, enmascarados en el disco", + "desktopOnly": "Secure Files está disponible en la aplicación de escritorio.", + "allFiles": "Todos los archivos", + "overview": "Resumen", + "statFiles": "Archivos", + "statFolders": "Carpetas", + "statContent": "Tamaño del contenido", + "statContentHint": "Tamaño total de los archivos originales", + "statOnDisk": "En disco", + "statOverhead": "{size} de sobrecarga de cifrado", + "byType": "Por tipo", + "fileCount": "{count, plural, one {# archivo} other {# archivos}}", + "largestFiles": "Archivos más grandes", + "recentlyAdded": "Añadidos recientemente", + "openContainingFolder": "Abrir la carpeta contenedora", + "types": { + "image": "Imágenes", + "video": "Vídeo", + "audio": "Audio", + "pdf": "PDF", + "archive": "Archivos comprimidos", + "code": "Código", + "doc": "Documentos", + "sheet": "Hojas de cálculo", + "file": "Otros" + }, + "breadcrumb": "Ruta de la carpeta", + "addFiles": "Añadir archivos", + "addFolder": "Añadir carpeta", + "importFolder": "Importar carpeta", + "newFolder": "Nueva carpeta", + "lock": "Bloquear", + "settings": "Ajustes de almacenamiento", + "storageFolder": "Carpeta de almacenamiento", + "notSet": "Sin definir", + "chooseFolder": "Elegir carpeta", + "changeFolder": "Cambiar carpeta…", + "chooseFolderTitle": "Elige dónde se guardan los archivos cifrados", + "chooseFolderBody": "Elige cualquier carpeta de este dispositivo o de una unidad externa. Fuera de MyDevTools solo contiene archivos .mydt enmascarados.", + "changeFolderTitle": "¿Mover los archivos cifrados?", + "changeFolderBody": "Todos los archivos .mydt se moverán de la carpeta de almacenamiento actual a la que elijas a continuación.", + "folderSet": "Carpeta de almacenamiento definida", + "folderMissingTitle": "No se encuentra la carpeta de almacenamiento", + "folderMissingBody": "«{dir}» no está disponible: puede estar en una unidad desconectada o proceder de una copia de seguridad restaurada de otro equipo. Vuelve a conectarla o elige una carpeta nueva.", + "movedCount": "Se movieron {count} archivos cifrados", + "importedCount": "Se cifraron {count} archivos", + "emptyFolderAdded": "Carpeta añadida: no contenía archivos que cifrar", + "emptyTitle": "Aquí todavía no hay archivos", + "emptyBody": "Añade archivos o una carpeta entera. Cada archivo se cifra en un objeto .mydt con nombre aleatorio; el nombre, el tipo y el contenido originales solo se ven aquí.", + "name": "Nombre", + "size": "Tamaño", + "modified": "Modificado", + "actions": "Acciones", + "expand": "Expandir", + "collapse": "Contraer", + "preview": "Vista previa", + "noPreview": "No hay vista previa para este tipo de archivo: expórtalo para abrirlo en otra aplicación.", + "export": "Exportar", + "exportWarningTitle": "¿Exportar una copia descifrada?", + "exportWarningBody": "El archivo se escribirá sin cifrar en la ubicación que elijas. Cualquier cosa en ese dispositivo podrá leerlo.", + "exported": "Copia descifrada exportada", + "replace": "Reemplazar contenido…", + "replaced": "Contenido del archivo reemplazado", + "rename": "Renombrar", + "fileName": "Nombre del archivo", + "move": "Mover a una carpeta", + "targetFolder": "Carpeta", + "targetFolderHint": "Usa barras para carpetas anidadas, por ejemplo proyecto/config. Déjalo vacío para el nivel superior.", + "folderName": "Nombre de la carpeta", + "delete": "Eliminar", + "deleted": "Archivo eliminado", + "deletedCount": "Se eliminaron {count} archivos", + "deleteFileTitle": "¿Eliminar este archivo?", + "deleteFileBody": "«{name}» se eliminará permanentemente de la carpeta de almacenamiento cifrada.", + "deleteFolderTitle": "¿Eliminar esta carpeta?", + "deleteFolderBody": "Todos los archivos dentro de «{dir}» se eliminarán permanentemente de la carpeta de almacenamiento cifrada.", + "unreadableCount": "Archivos ilegibles en la carpeta de almacenamiento: {count}", + "unreadableHint": "Se cifraron con otra contraseña maestra, pertenecen a otra bóveda o están dañados.", + "dismiss": "Descartar", + "viewGrid": "Vista de cuadrícula", + "viewList": "Vista de lista", + "footerNote": "Archivos de hasta 20 MB. Cifrados con Argon2id + XChaCha20-Poly1305; la clave solo permanece en memoria mientras la bóveda está desbloqueada.", + "cancel": "Cancelar", + "save": "Guardar" } } diff --git a/apps/desktop-ui/messages/fa.json b/apps/desktop-ui/messages/fa.json index 96fa10f4..10b2a386 100644 --- a/apps/desktop-ui/messages/fa.json +++ b/apps/desktop-ui/messages/fa.json @@ -76,7 +76,8 @@ "tokenCounter": "شمارنده توکن", "webhookTester": "آزمایشگر Webhook", "websocketTester": "آزمایشگر WebSocket", - "whoisLookup": "جست‌وجوی Whois" + "whoisLookup": "جست‌وجوی Whois", + "secureFiles": "پرونده‌ها" }, "Help": { "title": "راهنما و مستندات", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "ویرایش", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "در حال بازیابی…", "restoreSuccess": "نسخه پشتیبان بازیابی شد", "restoreError": "بازیابی ناموفق بود — عبارت عبور اشتباه یا فایل پشتیبان نامعتبر" + }, + "backupCodes": { + "title": "کدهای پشتیبان", + "description": "کدهای یک‌بارمصرف برای بازیابی گاوصندوق در صورت فراموشی رمز اصلی.", + "remaining": "{remaining} از {total} کد باقی مانده", + "none": "هیچ کد پشتیبانی ذخیره نشده است.", + "hint": "ساخت مجموعه جدید همه کدهای فعلی را باطل می‌کند.", + "passwordPlaceholder": "رمز اصلی", + "regenerateButton": "ساخت کدهای جدید", + "generating": "در حال ساخت…", + "wrongPassword": "رمز اصلی نادرست است", + "success": "کدهای پشتیبان جدید ساخته شد", + "error": "ساخت کدهای پشتیبان ناموفق بود", + "newCodesWarning": "دیگر نمی‌توانید آن‌ها را ببینید. اکنون دانلود یا کپی کنید.", + "downloadButton": "دانلود", + "doneButton": "کدهایم را ذخیره کردم" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "جست‌وجوی Whois", "description": "جزئیات ثبت دامنه و IP را از طریق RDAP جست‌وجو کنید." + }, + "secureFiles": { + "title": "پرونده‌ها", + "description": "پرونده‌ها و پوشه‌ها را به شیء‌های ‎.mydt‎ با نام پوشیده رمزگذاری کنید؛ پس از باز کردن قفل مرور و پیش‌نمایش کنید." } }, "tabs": { @@ -2278,7 +2300,8 @@ "responseCopied": "پاسخ در کلیپ بورد کپی شد", "codeCopied": "کد در کلیپ بورد کپی شد", "copyFailed": "کپی در کلیپ‌بورد ناموفق بود", - "curlCopied": "دستور cURL کپی شد" + "curlCopied": "دستور cURL کپی شد", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "مجموعه ها", @@ -2351,6 +2374,8 @@ "collectionsSidebar": { "title": "منابع API", "newCollection": "مجموعه جدید", + "openFolderCollection": "باز کردن پوشه به عنوان مجموعه", + "folderCollection": "مجموعه پوشه‌ای", "tabCollections": "مجموعه ها", "tabHistory": "تاریخچه", "noCollectionsTitle": "بدون مجموعه", @@ -2392,6 +2417,7 @@ "placeholderNewEnv": "محیط جدید", "variablesHeading": "{name} متغیرها", "addVariable": "متغیر اضافه کنید", + "exportPostman": "خروجی (محیط Postman)", "columnVariable": "متغیر", "columnValue": "ارزش", "placeholderKey": "کلید", @@ -2448,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5188,6 +5223,12 @@ "offline": "۱۰۰٪ آفلاین" }, "ToolSidebar": { + "expand": "گسترش نوار کناری", + "collapse": "جمع کردن نوار کناری", + "resize": "تغییر اندازه نوار کناری", + "resetWidth": "بازنشانی عرض نوار کناری", + "moreSections": "بخش‌های بیشتر", + "sections": "بخش‌های نوار کناری", "show": "نمایش نوار کناری", "hide": "پنهان کردن نوار کناری" }, @@ -5211,5 +5252,92 @@ "accept": "اشتراک‌گذاری ناشناس", "decline": "نه، ممنون" } + }, + "SecureFiles": { + "title": "پرونده‌ها", + "subtitle": "پرونده‌هایی که تا زمانی که قفلشان را باز نکنید خصوصی می‌مانند — روی دستگاه شما رمزگذاری‌شده و روی دیسک پوشیده", + "desktopOnly": "‏Secure Files در برنامهٔ رومیزی در دسترس است.", + "allFiles": "همهٔ پرونده‌ها", + "overview": "نمای کلی", + "statFiles": "پرونده‌ها", + "statFolders": "پوشه‌ها", + "statContent": "حجم محتوا", + "statContentHint": "حجم کل پرونده‌های اصلی", + "statOnDisk": "روی دیسک", + "statOverhead": "‏{size} سربار رمزگذاری", + "byType": "بر پایهٔ نوع", + "fileCount": "{count, plural, one {# پرونده} other {# پرونده}}", + "largestFiles": "بزرگ‌ترین پرونده‌ها", + "recentlyAdded": "به‌تازگی افزوده‌شده", + "openContainingFolder": "باز کردن پوشهٔ دربرگیرنده", + "types": { + "image": "تصاویر", + "video": "ویدیو", + "audio": "صدا", + "pdf": "PDF", + "archive": "بایگانی‌ها", + "code": "کد", + "doc": "سندها", + "sheet": "صفحه‌گسترده‌ها", + "file": "سایر" + }, + "breadcrumb": "مسیر پوشه", + "addFiles": "افزودن پرونده", + "addFolder": "افزودن پوشه", + "importFolder": "درون‌ریزی پوشه", + "newFolder": "پوشهٔ جدید", + "lock": "قفل کردن", + "settings": "تنظیمات ذخیره‌سازی", + "storageFolder": "پوشهٔ ذخیره‌سازی", + "notSet": "تعیین‌نشده", + "chooseFolder": "انتخاب پوشه", + "changeFolder": "تغییر پوشه…", + "chooseFolderTitle": "انتخاب کنید پرونده‌های رمزگذاری‌شده کجا ذخیره شوند", + "chooseFolderBody": "هر پوشه‌ای روی این دستگاه یا یک درایو خارجی را انتخاب کنید. بیرون از MyDevTools تنها شامل پرونده‌های ‎.mydt‎ با نام پوشیده است.", + "changeFolderTitle": "پرونده‌های رمزگذاری‌شده جابه‌جا شوند؟", + "changeFolderBody": "همهٔ پرونده‌های ‎.mydt‎ از پوشهٔ ذخیره‌سازی کنونی به پوشه‌ای که در ادامه انتخاب می‌کنید منتقل می‌شوند.", + "folderSet": "پوشهٔ ذخیره‌سازی تعیین شد", + "folderMissingTitle": "پوشهٔ ذخیره‌سازی پیدا نشد", + "folderMissingBody": "‏«{dir}» در دسترس نیست — ممکن است روی درایوی جدا‌شده باشد یا از پشتیبان بازیابی‌شدهٔ رایانه‌ای دیگر بیاید. دوباره وصلش کنید یا پوشه‌ای تازه برگزینید.", + "movedCount": "‏{count} پروندهٔ رمزگذاری‌شده منتقل شد", + "importedCount": "‏{count} پرونده رمزگذاری شد", + "emptyFolderAdded": "پوشه افزوده شد — پرونده‌ای برای رمزگذاری نداشت", + "emptyTitle": "هنوز پرونده‌ای اینجا نیست", + "emptyBody": "پرونده یا یک پوشهٔ کامل بیفزایید. هر پرونده به شیء ‎.mydt‎ با نامی تصادفی رمزگذاری می‌شود؛ نام، نوع و محتوای اصلی تنها همین‌جا دیده می‌شوند.", + "name": "نام", + "size": "اندازه", + "modified": "تغییریافته", + "actions": "کنش‌ها", + "expand": "گستردن", + "collapse": "جمع کردن", + "preview": "پیش‌نمایش", + "noPreview": "برای این نوع پرونده پیش‌نمایشی نیست — آن را برون‌ریزی کنید تا در برنامه‌ای دیگر باز شود.", + "export": "برون‌ریزی", + "exportWarningTitle": "یک نسخهٔ رمزگشایی‌شده برون‌ریزی شود؟", + "exportWarningBody": "پرونده بدون رمزگذاری در مکانی که برمی‌گزینید نوشته می‌شود. هر چیزی روی آن دستگاه می‌تواند آن را بخواند.", + "exported": "نسخهٔ رمزگشایی‌شده برون‌ریزی شد", + "replace": "جایگزینی محتوا…", + "replaced": "محتوای پرونده جایگزین شد", + "rename": "تغییر نام", + "fileName": "نام پرونده", + "move": "انتقال به پوشه", + "targetFolder": "پوشه", + "targetFolderHint": "برای پوشه‌های تودرتو از اسلش استفاده کنید، مانند project/config. برای بالاترین سطح خالی بگذارید.", + "folderName": "نام پوشه", + "delete": "حذف", + "deleted": "پرونده حذف شد", + "deletedCount": "‏{count} پرونده حذف شد", + "deleteFileTitle": "این پرونده حذف شود؟", + "deleteFileBody": "‏«{name}» برای همیشه از پوشهٔ ذخیره‌سازی رمزگذاری‌شده حذف می‌شود.", + "deleteFolderTitle": "این پوشه حذف شود؟", + "deleteFolderBody": "همهٔ پرونده‌های درون «{dir}» برای همیشه از پوشهٔ ذخیره‌سازی رمزگذاری‌شده حذف می‌شوند.", + "unreadableCount": "پرونده‌های ناخوانا در پوشهٔ ذخیره‌سازی: {count}", + "unreadableHint": "با گذرواژهٔ اصلی دیگری رمزگذاری شده‌اند، به گاوصندوق دیگری تعلق دارند، یا آسیب دیده‌اند.", + "dismiss": "بستن", + "viewGrid": "نمای شبکه‌ای", + "viewList": "نمای فهرستی", + "footerNote": "پرونده‌های تا ۲۰ مگابایت. رمزگذاری با Argon2id + XChaCha20-Poly1305؛ کلید تنها تا زمانی که گاوصندوق باز است در حافظه می‌ماند.", + "cancel": "انصراف", + "save": "ذخیره" } } diff --git a/apps/desktop-ui/messages/fr.json b/apps/desktop-ui/messages/fr.json index e6ed772a..e4420437 100644 --- a/apps/desktop-ui/messages/fr.json +++ b/apps/desktop-ui/messages/fr.json @@ -76,7 +76,8 @@ "tokenCounter": "Compteur de tokens", "webhookTester": "Testeur de webhook", "websocketTester": "Testeur WebSocket", - "whoisLookup": "Recherche Whois" + "whoisLookup": "Recherche Whois", + "secureFiles": "Fichiers" }, "Help": { "title": "Aide & documentation", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Modifier", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Restauration…", "restoreSuccess": "Sauvegarde restaurée", "restoreError": "Échec de la restauration — phrase secrète incorrecte ou fichier non valide" + }, + "backupCodes": { + "title": "Codes de secours", + "description": "Codes à usage unique pour récupérer votre coffre si vous oubliez votre mot de passe maître.", + "remaining": "{remaining} sur {total} codes restants", + "none": "Aucun code de secours enregistré.", + "hint": "Générer une nouvelle série invalide tous les codes existants.", + "passwordPlaceholder": "Mot de passe maître", + "regenerateButton": "Générer de nouveaux codes", + "generating": "Génération…", + "wrongPassword": "Mot de passe maître incorrect", + "success": "Nouveaux codes de secours générés", + "error": "Impossible de générer les codes de secours", + "newCodesWarning": "Vous ne pourrez plus les afficher. Téléchargez-les ou copiez-les maintenant.", + "downloadButton": "Télécharger", + "doneButton": "J'ai enregistré mes codes" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Recherche Whois", "description": "Consultez les détails d'enregistrement d'un domaine ou d'une IP via RDAP." + }, + "secureFiles": { + "title": "Fichiers", + "description": "Chiffrez fichiers et dossiers en objets .mydt masqués ; parcourez-les et prévisualisez-les après déverrouillage." } }, "tabs": { @@ -2278,7 +2300,8 @@ "responseCopied": "Réponse copiée", "codeCopied": "Code copié", "copyFailed": "Impossible de copier dans le presse-papiers", - "curlCopied": "Commande cURL copiée" + "curlCopied": "Commande cURL copiée", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Collections", @@ -2351,6 +2374,8 @@ "collectionsSidebar": { "title": "Ressources API", "newCollection": "Nouvelle collection", + "openFolderCollection": "Ouvrir un dossier comme collection", + "folderCollection": "Collection de dossier", "tabCollections": "Collections", "tabHistory": "Historique", "noCollectionsTitle": "Aucune collection", @@ -2392,6 +2417,7 @@ "placeholderNewEnv": "Nouvel environnement", "variablesHeading": "Variables {name}", "addVariable": "Ajouter une variable", + "exportPostman": "Exporter (environnement Postman)", "columnVariable": "Variable", "columnValue": "Valeur", "placeholderKey": "Clé", @@ -2448,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5188,6 +5223,12 @@ "offline": "100 % hors ligne" }, "ToolSidebar": { + "expand": "Développer la barre latérale", + "collapse": "Réduire la barre latérale", + "resize": "Redimensionner la barre latérale", + "resetWidth": "Réinitialiser la largeur", + "moreSections": "Plus de sections", + "sections": "Sections de la barre latérale", "show": "Afficher la barre latérale", "hide": "Masquer la barre latérale" }, @@ -5211,5 +5252,92 @@ "accept": "Partager anonymement", "decline": "Non merci" } + }, + "SecureFiles": { + "title": "Fichiers", + "subtitle": "Des fichiers qui restent privés jusqu'à leur déverrouillage — chiffrés sur votre appareil, masqués sur le disque", + "desktopOnly": "Secure Files est disponible dans l'application de bureau.", + "allFiles": "Tous les fichiers", + "overview": "Vue d'ensemble", + "statFiles": "Fichiers", + "statFolders": "Dossiers", + "statContent": "Taille du contenu", + "statContentHint": "Taille totale des fichiers d'origine", + "statOnDisk": "Sur le disque", + "statOverhead": "{size} de surcoût de chiffrement", + "byType": "Par type", + "fileCount": "{count, plural, one {# fichier} other {# fichiers}}", + "largestFiles": "Fichiers les plus volumineux", + "recentlyAdded": "Ajoutés récemment", + "openContainingFolder": "Ouvrir le dossier parent", + "types": { + "image": "Images", + "video": "Vidéo", + "audio": "Audio", + "pdf": "PDF", + "archive": "Archives", + "code": "Code", + "doc": "Documents", + "sheet": "Feuilles de calcul", + "file": "Autres" + }, + "breadcrumb": "Chemin du dossier", + "addFiles": "Ajouter des fichiers", + "addFolder": "Ajouter un dossier", + "importFolder": "Importer un dossier", + "newFolder": "Nouveau dossier", + "lock": "Verrouiller", + "settings": "Paramètres de stockage", + "storageFolder": "Dossier de stockage", + "notSet": "Non défini", + "chooseFolder": "Choisir un dossier", + "changeFolder": "Changer de dossier…", + "chooseFolderTitle": "Choisissez où stocker les fichiers chiffrés", + "chooseFolderBody": "Choisissez n'importe quel dossier sur cet appareil ou sur un disque externe. En dehors de MyDevTools, il ne contient que des fichiers .mydt masqués.", + "changeFolderTitle": "Déplacer les fichiers chiffrés ?", + "changeFolderBody": "Tous les fichiers .mydt seront déplacés du dossier de stockage actuel vers celui que vous choisirez ensuite.", + "folderSet": "Dossier de stockage défini", + "folderMissingTitle": "Dossier de stockage introuvable", + "folderMissingBody": "« {dir} » n'est pas disponible — il se trouve peut-être sur un disque débranché ou provient d'une sauvegarde restaurée depuis une autre machine. Reconnectez-le ou choisissez un nouveau dossier.", + "movedCount": "{count} fichiers chiffrés déplacés", + "importedCount": "{count} fichiers chiffrés", + "emptyFolderAdded": "Dossier ajouté — il ne contenait aucun fichier à chiffrer", + "emptyTitle": "Aucun fichier ici pour l'instant", + "emptyBody": "Ajoutez des fichiers ou un dossier entier. Chaque fichier est chiffré dans un objet .mydt au nom aléatoire ; le nom, le type et le contenu d'origine ne sont visibles qu'ici.", + "name": "Nom", + "size": "Taille", + "modified": "Modifié", + "actions": "Actions", + "expand": "Développer", + "collapse": "Réduire", + "preview": "Aperçu", + "noPreview": "Aucun aperçu pour ce type de fichier — exportez-le pour l'ouvrir dans une autre application.", + "export": "Exporter", + "exportWarningTitle": "Exporter une copie déchiffrée ?", + "exportWarningBody": "Le fichier sera écrit en clair à l'emplacement choisi. Tout ce qui se trouve sur cet appareil pourra le lire.", + "exported": "Copie déchiffrée exportée", + "replace": "Remplacer le contenu…", + "replaced": "Contenu du fichier remplacé", + "rename": "Renommer", + "fileName": "Nom du fichier", + "move": "Déplacer vers un dossier", + "targetFolder": "Dossier", + "targetFolderHint": "Utilisez des barres obliques pour les dossiers imbriqués, par exemple projet/config. Laissez vide pour le niveau supérieur.", + "folderName": "Nom du dossier", + "delete": "Supprimer", + "deleted": "Fichier supprimé", + "deletedCount": "{count} fichiers supprimés", + "deleteFileTitle": "Supprimer ce fichier ?", + "deleteFileBody": "« {name} » sera définitivement retiré du dossier de stockage chiffré.", + "deleteFolderTitle": "Supprimer ce dossier ?", + "deleteFolderBody": "Tous les fichiers sous « {dir} » seront définitivement retirés du dossier de stockage chiffré.", + "unreadableCount": "Fichiers illisibles dans le dossier de stockage : {count}", + "unreadableHint": "Ils ont été chiffrés avec un autre mot de passe maître, appartiennent à un autre coffre-fort ou sont corrompus.", + "dismiss": "Ignorer", + "viewGrid": "Vue en grille", + "viewList": "Vue en liste", + "footerNote": "Fichiers jusqu'à 20 Mo. Chiffrés avec Argon2id + XChaCha20-Poly1305 ; la clé ne réside en mémoire que pendant le déverrouillage du coffre-fort.", + "cancel": "Annuler", + "save": "Enregistrer" } } diff --git a/apps/desktop-ui/messages/id.json b/apps/desktop-ui/messages/id.json index f57d3a7e..ba78f61d 100644 --- a/apps/desktop-ui/messages/id.json +++ b/apps/desktop-ui/messages/id.json @@ -76,7 +76,8 @@ "tokenCounter": "Penghitung Token", "webhookTester": "Penguji Webhook", "websocketTester": "Penguji WebSocket", - "whoisLookup": "Pencarian Whois" + "whoisLookup": "Pencarian Whois", + "secureFiles": "Berkas" }, "Help": { "title": "Bantuan & Dokumentasi", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Ubah", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Memulihkan…", "restoreSuccess": "Cadangan dipulihkan", "restoreError": "Pemulihan gagal — frasa sandi salah atau file cadangan tidak valid" + }, + "backupCodes": { + "title": "Kode cadangan", + "description": "Kode sekali pakai untuk memulihkan brankas jika Anda lupa kata sandi utama.", + "remaining": "Tersisa {remaining} dari {total} kode", + "none": "Belum ada kode cadangan tersimpan.", + "hint": "Membuat set baru membatalkan semua kode yang ada.", + "passwordPlaceholder": "Kata sandi utama", + "regenerateButton": "Buat kode baru", + "generating": "Membuat…", + "wrongPassword": "Kata sandi utama salah", + "success": "Kode cadangan baru dibuat", + "error": "Gagal membuat kode cadangan", + "newCodesWarning": "Anda tidak dapat melihatnya lagi. Unduh atau salin sekarang.", + "downloadButton": "Unduh", + "doneButton": "Saya sudah menyimpan kode" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Pencarian Whois", "description": "Cari detail pendaftaran domain dan IP melalui RDAP." + }, + "secureFiles": { + "title": "Berkas", + "description": "Enkripsi berkas dan folder menjadi objek .mydt bernama samar; jelajahi dan pratinjau setelah dibuka." } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "Mengambil sampel {docs} dokumen · {fields} bidang", "colField": "Bidang", "colTypes": "Tipe", - "colCoverage": "Cakupan" + "colCoverage": "Cakupan", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "Tanggapan disalin ke clipboard", "codeCopied": "Disalin ke papan klip", "copyFailed": "Gagal menyalin ke papan klip", - "curlCopied": "Perintah cURL disalin" + "curlCopied": "Perintah cURL disalin", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Penagihan", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "API resources", "newCollection": "New collection", + "openFolderCollection": "Buka folder sebagai koleksi", + "folderCollection": "Koleksi folder", "tabCollections": "Collections", "tabHistory": "History", "noCollectionsTitle": "No collections", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "New environment", "variablesHeading": "{name} variables", "addVariable": "Add variable", + "exportPostman": "Ekspor (lingkungan Postman)", "columnVariable": "Variable", "columnValue": "Value", "placeholderKey": "Key", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100% offline" }, "ToolSidebar": { + "expand": "Bentangkan bilah sisi", + "collapse": "Ciutkan bilah sisi", + "resize": "Ubah ukuran bilah sisi", + "resetWidth": "Atur ulang lebar bilah sisi", + "moreSections": "Bagian lainnya", + "sections": "Bagian bilah sisi", "show": "Tampilkan bilah sisi", "hide": "Sembunyikan bilah sisi" }, @@ -5200,5 +5252,92 @@ "accept": "Bagikan secara anonim", "decline": "Tidak, terima kasih" } + }, + "SecureFiles": { + "title": "Berkas", + "subtitle": "Berkas yang tetap pribadi sampai Anda membukanya — dienkripsi di perangkat Anda, disamarkan di disk", + "desktopOnly": "Secure Files tersedia di aplikasi desktop.", + "allFiles": "Semua berkas", + "overview": "Ringkasan", + "statFiles": "Berkas", + "statFolders": "Folder", + "statContent": "Ukuran konten", + "statContentHint": "Ukuran total berkas asli", + "statOnDisk": "Di disk", + "statOverhead": "{size} overhead enkripsi", + "byType": "Menurut jenis", + "fileCount": "{count, plural, other {# berkas}}", + "largestFiles": "Berkas terbesar", + "recentlyAdded": "Baru ditambahkan", + "openContainingFolder": "Buka folder induk", + "types": { + "image": "Gambar", + "video": "Video", + "audio": "Audio", + "pdf": "PDF", + "archive": "Arsip", + "code": "Kode", + "doc": "Dokumen", + "sheet": "Lembar kerja", + "file": "Lainnya" + }, + "breadcrumb": "Jalur folder", + "addFiles": "Tambah berkas", + "addFolder": "Tambah folder", + "importFolder": "Impor folder", + "newFolder": "Folder baru", + "lock": "Kunci", + "settings": "Pengaturan penyimpanan", + "storageFolder": "Folder penyimpanan", + "notSet": "Belum diatur", + "chooseFolder": "Pilih folder", + "changeFolder": "Ganti folder…", + "chooseFolderTitle": "Pilih tempat penyimpanan berkas terenkripsi", + "chooseFolderBody": "Pilih folder mana pun di perangkat ini atau di drive eksternal. Di luar MyDevTools, folder itu hanya berisi berkas .mydt bernama samar.", + "changeFolderTitle": "Pindahkan berkas terenkripsi?", + "changeFolderBody": "Semua berkas .mydt akan dipindahkan dari folder penyimpanan saat ini ke folder yang Anda pilih berikutnya.", + "folderSet": "Folder penyimpanan diatur", + "folderMissingTitle": "Folder penyimpanan tidak ditemukan", + "folderMissingBody": "“{dir}” tidak tersedia — mungkin berada di drive yang dicabut atau berasal dari cadangan yang dipulihkan dari mesin lain. Sambungkan kembali, atau pilih folder baru.", + "movedCount": "{count} berkas terenkripsi dipindahkan", + "importedCount": "{count} berkas dienkripsi", + "emptyFolderAdded": "Folder ditambahkan — tidak ada berkas untuk dienkripsi", + "emptyTitle": "Belum ada berkas di sini", + "emptyBody": "Tambahkan berkas atau seluruh folder. Setiap berkas dienkripsi menjadi objek .mydt bernama acak; nama, jenis, dan isi aslinya hanya terlihat di sini.", + "name": "Nama", + "size": "Ukuran", + "modified": "Diubah", + "actions": "Tindakan", + "expand": "Bentangkan", + "collapse": "Ciutkan", + "preview": "Pratinjau", + "noPreview": "Tidak ada pratinjau untuk jenis berkas ini — ekspor untuk membukanya di aplikasi lain.", + "export": "Ekspor", + "exportWarningTitle": "Ekspor salinan yang sudah didekripsi?", + "exportWarningBody": "Berkas akan ditulis tanpa enkripsi ke lokasi yang Anda pilih. Apa pun di perangkat itu bisa membacanya.", + "exported": "Salinan terdekripsi diekspor", + "replace": "Ganti isi…", + "replaced": "Isi berkas diganti", + "rename": "Ganti nama", + "fileName": "Nama berkas", + "move": "Pindahkan ke folder", + "targetFolder": "Folder", + "targetFolderHint": "Gunakan garis miring untuk folder bersarang, misalnya proyek/config. Kosongkan untuk tingkat teratas.", + "folderName": "Nama folder", + "delete": "Hapus", + "deleted": "Berkas dihapus", + "deletedCount": "{count} berkas dihapus", + "deleteFileTitle": "Hapus berkas ini?", + "deleteFileBody": "“{name}” akan dihapus permanen dari folder penyimpanan terenkripsi.", + "deleteFolderTitle": "Hapus folder ini?", + "deleteFolderBody": "Setiap berkas di dalam “{dir}” akan dihapus permanen dari folder penyimpanan terenkripsi.", + "unreadableCount": "Berkas tak terbaca di folder penyimpanan: {count}", + "unreadableHint": "Berkas itu dienkripsi dengan kata sandi utama lain, milik brankas lain, atau rusak.", + "dismiss": "Tutup", + "viewGrid": "Tampilan kisi", + "viewList": "Tampilan daftar", + "footerNote": "Berkas hingga 20 MB. Dienkripsi dengan Argon2id + XChaCha20-Poly1305; kunci hanya ada di memori selama brankas terbuka.", + "cancel": "Batal", + "save": "Simpan" } } diff --git a/apps/desktop-ui/messages/it.json b/apps/desktop-ui/messages/it.json index 8c2dbcdc..5cf8185b 100644 --- a/apps/desktop-ui/messages/it.json +++ b/apps/desktop-ui/messages/it.json @@ -76,7 +76,8 @@ "tokenCounter": "Contatore di token", "webhookTester": "Tester di webhook", "websocketTester": "Tester WebSocket", - "whoisLookup": "Ricerca Whois" + "whoisLookup": "Ricerca Whois", + "secureFiles": "File" }, "Help": { "title": "Guida e documentazione", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Modifica", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Ripristino…", "restoreSuccess": "Backup ripristinato", "restoreError": "Ripristino non riuscito — passphrase errata o file di backup non valido" + }, + "backupCodes": { + "title": "Codici di backup", + "description": "Codici monouso che recuperano la cassaforte se dimentichi la password principale.", + "remaining": "{remaining} di {total} codici rimasti", + "none": "Nessun codice di backup salvato.", + "hint": "Generare nuovi codici invalida tutti quelli esistenti.", + "passwordPlaceholder": "Password principale", + "regenerateButton": "Genera nuovi codici", + "generating": "Generazione…", + "wrongPassword": "Password principale errata", + "success": "Nuovi codici di backup generati", + "error": "Impossibile generare i codici di backup", + "newCodesWarning": "Non potrai più visualizzarli. Scaricali o copiali ora.", + "downloadButton": "Scarica", + "doneButton": "Ho salvato i codici" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Ricerca Whois", "description": "Cerca i dettagli di registrazione di domini e IP tramite RDAP." + }, + "secureFiles": { + "title": "File", + "description": "Cifra file e cartelle in oggetti .mydt mascherati; sfogliali e visualizzali dopo lo sblocco." } }, "tabs": { @@ -2278,7 +2300,8 @@ "responseCopied": "Risposta copiata negli appunti", "codeCopied": "Codice copiato negli appunti", "copyFailed": "Copia negli appunti non riuscita", - "curlCopied": "Comando cURL copiato" + "curlCopied": "Comando cURL copiato", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Collezioni", @@ -2351,6 +2374,8 @@ "collectionsSidebar": { "title": "Risorse API", "newCollection": "Nuova collezione", + "openFolderCollection": "Apri cartella come raccolta", + "folderCollection": "Raccolta da cartella", "tabCollections": "Collezioni", "tabHistory": "Storia", "noCollectionsTitle": "Nessuna raccolta", @@ -2392,6 +2417,7 @@ "placeholderNewEnv": "Nuovo ambiente", "variablesHeading": "{name} variabili", "addVariable": "Aggiungi variabile", + "exportPostman": "Esporta (ambiente Postman)", "columnVariable": "Variabile", "columnValue": "Valore", "placeholderKey": "Chiave", @@ -2448,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5188,6 +5223,12 @@ "offline": "100% offline" }, "ToolSidebar": { + "expand": "Espandi barra laterale", + "collapse": "Comprimi barra laterale", + "resize": "Ridimensiona barra laterale", + "resetWidth": "Reimposta larghezza", + "moreSections": "Altre sezioni", + "sections": "Sezioni della barra laterale", "show": "Mostra barra laterale", "hide": "Nascondi barra laterale" }, @@ -5211,5 +5252,92 @@ "accept": "Condividi in forma anonima", "decline": "No, grazie" } + }, + "SecureFiles": { + "title": "File", + "subtitle": "File che restano privati finché non li sblocchi — cifrati sul tuo dispositivo, mascherati sul disco", + "desktopOnly": "Secure Files è disponibile nell'app desktop.", + "allFiles": "Tutti i file", + "overview": "Panoramica", + "statFiles": "File", + "statFolders": "Cartelle", + "statContent": "Dimensione dei contenuti", + "statContentHint": "Dimensione totale dei file originali", + "statOnDisk": "Su disco", + "statOverhead": "{size} di overhead di cifratura", + "byType": "Per tipo", + "fileCount": "{count, plural, one {# file} other {# file}}", + "largestFiles": "File più grandi", + "recentlyAdded": "Aggiunti di recente", + "openContainingFolder": "Apri la cartella che lo contiene", + "types": { + "image": "Immagini", + "video": "Video", + "audio": "Audio", + "pdf": "PDF", + "archive": "Archivi", + "code": "Codice", + "doc": "Documenti", + "sheet": "Fogli di calcolo", + "file": "Altro" + }, + "breadcrumb": "Percorso della cartella", + "addFiles": "Aggiungi file", + "addFolder": "Aggiungi cartella", + "importFolder": "Importa cartella", + "newFolder": "Nuova cartella", + "lock": "Blocca", + "settings": "Impostazioni di archiviazione", + "storageFolder": "Cartella di archiviazione", + "notSet": "Non impostata", + "chooseFolder": "Scegli cartella", + "changeFolder": "Cambia cartella…", + "chooseFolderTitle": "Scegli dove archiviare i file cifrati", + "chooseFolderBody": "Scegli una cartella qualsiasi su questo dispositivo o su un'unità esterna. Fuori da MyDevTools contiene soltanto file .mydt mascherati.", + "changeFolderTitle": "Spostare i file cifrati?", + "changeFolderBody": "Tutti i file .mydt verranno spostati dalla cartella di archiviazione attuale a quella che sceglierai ora.", + "folderSet": "Cartella di archiviazione impostata", + "folderMissingTitle": "Cartella di archiviazione non trovata", + "folderMissingBody": "«{dir}» non è disponibile — potrebbe trovarsi su un'unità scollegata o provenire da un backup ripristinato da un altro computer. Ricollegala oppure scegli una nuova cartella.", + "movedCount": "Spostati {count} file cifrati", + "importedCount": "Cifrati {count} file", + "emptyFolderAdded": "Cartella aggiunta — non conteneva file da cifrare", + "emptyTitle": "Qui non ci sono ancora file", + "emptyBody": "Aggiungi file o un'intera cartella. Ogni file viene cifrato in un oggetto .mydt con nome casuale; nome, tipo e contenuto originali sono visibili solo qui.", + "name": "Nome", + "size": "Dimensione", + "modified": "Modificato", + "actions": "Azioni", + "expand": "Espandi", + "collapse": "Comprimi", + "preview": "Anteprima", + "noPreview": "Nessuna anteprima per questo tipo di file — esportalo per aprirlo in un'altra app.", + "export": "Esporta", + "exportWarningTitle": "Esportare una copia decifrata?", + "exportWarningBody": "Il file verrà scritto in chiaro nella posizione scelta. Qualsiasi cosa su quel dispositivo potrà leggerlo.", + "exported": "Copia decifrata esportata", + "replace": "Sostituisci contenuto…", + "replaced": "Contenuto del file sostituito", + "rename": "Rinomina", + "fileName": "Nome del file", + "move": "Sposta in una cartella", + "targetFolder": "Cartella", + "targetFolderHint": "Usa le barre per le cartelle annidate, ad esempio progetto/config. Lascia vuoto per il livello principale.", + "folderName": "Nome della cartella", + "delete": "Elimina", + "deleted": "File eliminato", + "deletedCount": "Eliminati {count} file", + "deleteFileTitle": "Eliminare questo file?", + "deleteFileBody": "«{name}» verrà rimosso definitivamente dalla cartella di archiviazione cifrata.", + "deleteFolderTitle": "Eliminare questa cartella?", + "deleteFolderBody": "Ogni file in «{dir}» verrà rimosso definitivamente dalla cartella di archiviazione cifrata.", + "unreadableCount": "File illeggibili nella cartella di archiviazione: {count}", + "unreadableHint": "Sono stati cifrati con un'altra password principale, appartengono a un'altra cassaforte o sono danneggiati.", + "dismiss": "Ignora", + "viewGrid": "Vista a griglia", + "viewList": "Vista a elenco", + "footerNote": "File fino a 20 MB. Cifrati con Argon2id + XChaCha20-Poly1305; la chiave resta in memoria solo finché la cassaforte è sbloccata.", + "cancel": "Annulla", + "save": "Salva" } } diff --git a/apps/desktop-ui/messages/ja.json b/apps/desktop-ui/messages/ja.json index 30f0a028..2fe81b64 100644 --- a/apps/desktop-ui/messages/ja.json +++ b/apps/desktop-ui/messages/ja.json @@ -76,7 +76,8 @@ "tokenCounter": "トークンカウンター", "webhookTester": "Webhook テスター", "websocketTester": "WebSocket テスター", - "whoisLookup": "Whois 検索" + "whoisLookup": "Whois 検索", + "secureFiles": "ファイル" }, "Help": { "title": "ヘルプとドキュメント", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "編集", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "復元中…", "restoreSuccess": "バックアップを復元しました", "restoreError": "復元に失敗しました — パスフレーズが違うか、無効なバックアップファイルです" + }, + "backupCodes": { + "title": "バックアップコード", + "description": "マスターパスワードを忘れたときにボルトを復旧できる、1 回だけ使えるコードです。", + "remaining": "残り {remaining} / {total} コード", + "none": "バックアップコードは保存されていません。", + "hint": "新しく生成すると、既存のコードはすべて無効になります。", + "passwordPlaceholder": "マスターパスワード", + "regenerateButton": "新しいコードを生成", + "generating": "生成中…", + "wrongPassword": "マスターパスワードが違います", + "success": "新しいバックアップコードを生成しました", + "error": "バックアップコードを生成できませんでした", + "newCodesWarning": "この画面を離れると再表示できません。今すぐダウンロードまたはコピーしてください。", + "downloadButton": "ダウンロード", + "doneButton": "コードを保存しました" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Whois 検索", "description": "RDAP を使ってドメインと IP の登録情報を調べます。" + }, + "secureFiles": { + "title": "ファイル", + "description": "ファイルやフォルダーを名前が伏せられた .mydt オブジェクトに暗号化。ロック解除後に閲覧・プレビューできます。" } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "{docs} 件のドキュメントをサンプリング · {fields} 個のフィールド", "colField": "フィールド", "colTypes": "型", - "colCoverage": "カバレッジ" + "colCoverage": "カバレッジ", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "レスポンスをクリップボードにコピーしました", "codeCopied": "コードをクリップボードにコピーしました", "copyFailed": "クリップボードへのコピーに失敗しました", - "curlCopied": "cURLコマンドをコピーしました" + "curlCopied": "cURLコマンドをコピーしました", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "コレクション", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "APIリソース", "newCollection": "新しいコレクション", + "openFolderCollection": "フォルダーをコレクションとして開く", + "folderCollection": "フォルダーコレクション", "tabCollections": "コレクション", "tabHistory": "履歴", "noCollectionsTitle": "コレクションなし", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "新しい環境", "variablesHeading": "{name}の変数", "addVariable": "変数を追加", + "exportPostman": "エクスポート(Postman 環境)", "columnVariable": "変数", "columnValue": "値", "placeholderKey": "キー", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "完全オフライン" }, "ToolSidebar": { + "expand": "サイドバーを展開", + "collapse": "サイドバーを折りたたむ", + "resize": "サイドバーの幅を変更", + "resetWidth": "サイドバーの幅をリセット", + "moreSections": "その他のセクション", + "sections": "サイドバーのセクション", "show": "サイドバーを表示", "hide": "サイドバーを非表示" }, @@ -5200,5 +5252,92 @@ "accept": "匿名で共有", "decline": "共有しない" } + }, + "SecureFiles": { + "title": "ファイル", + "subtitle": "ロックを解除するまで非公開のままのファイル — 端末上で暗号化され、ディスク上では名前が伏せられます", + "desktopOnly": "Secure Files はデスクトップアプリでご利用いただけます。", + "allFiles": "すべてのファイル", + "overview": "概要", + "statFiles": "ファイル", + "statFolders": "フォルダー", + "statContent": "コンテンツのサイズ", + "statContentHint": "元のファイルの合計サイズ", + "statOnDisk": "ディスク上", + "statOverhead": "暗号化のオーバーヘッド {size}", + "byType": "種類別", + "fileCount": "{count, plural, other {# 個のファイル}}", + "largestFiles": "サイズの大きいファイル", + "recentlyAdded": "最近追加", + "openContainingFolder": "含まれるフォルダーを開く", + "types": { + "image": "画像", + "video": "動画", + "audio": "音声", + "pdf": "PDF", + "archive": "アーカイブ", + "code": "コード", + "doc": "ドキュメント", + "sheet": "スプレッドシート", + "file": "その他" + }, + "breadcrumb": "フォルダーのパス", + "addFiles": "ファイルを追加", + "addFolder": "フォルダーを追加", + "importFolder": "フォルダーを取り込む", + "newFolder": "新しいフォルダー", + "lock": "ロック", + "settings": "保存設定", + "storageFolder": "保存フォルダー", + "notSet": "未設定", + "chooseFolder": "フォルダーを選択", + "changeFolder": "フォルダーを変更…", + "chooseFolderTitle": "暗号化ファイルの保存先を選択", + "chooseFolderBody": "この端末または外付けドライブの任意のフォルダーを選んでください。MyDevTools の外からは、名前が伏せられた .mydt ファイルしか見えません。", + "changeFolderTitle": "暗号化ファイルを移動しますか?", + "changeFolderBody": "すべての .mydt ファイルが現在の保存フォルダーから、次に選ぶフォルダーへ移動されます。", + "folderSet": "保存フォルダーを設定しました", + "folderMissingTitle": "保存フォルダーが見つかりません", + "folderMissingBody": "「{dir}」は利用できません。取り外されたドライブ上にあるか、別のマシンから復元したバックアップのものである可能性があります。再接続するか、新しいフォルダーを選んでください。", + "movedCount": "暗号化ファイル {count} 個を移動しました", + "importedCount": "{count} 個のファイルを暗号化しました", + "emptyFolderAdded": "フォルダーを追加しました — 暗号化するファイルはありませんでした", + "emptyTitle": "ここにはまだファイルがありません", + "emptyBody": "ファイルまたはフォルダー全体を追加してください。各ファイルはランダムな名前の .mydt オブジェクトに暗号化され、元の名前・種類・内容はここでのみ表示されます。", + "name": "名前", + "size": "サイズ", + "modified": "更新日", + "actions": "操作", + "expand": "展開", + "collapse": "折りたたむ", + "preview": "プレビュー", + "noPreview": "この種類のファイルはプレビューできません — 書き出して別のアプリで開いてください。", + "export": "書き出す", + "exportWarningTitle": "復号したコピーを書き出しますか?", + "exportWarningBody": "選択した場所にファイルが暗号化されないまま書き込まれます。その端末上の何からでも読み取れる状態になります。", + "exported": "復号したコピーを書き出しました", + "replace": "内容を置き換える…", + "replaced": "ファイルの内容を置き換えました", + "rename": "名前を変更", + "fileName": "ファイル名", + "move": "フォルダーへ移動", + "targetFolder": "フォルダー", + "targetFolderHint": "入れ子のフォルダーはスラッシュで指定します(例: project/config)。最上位に置く場合は空のままにします。", + "folderName": "フォルダー名", + "delete": "削除", + "deleted": "ファイルを削除しました", + "deletedCount": "{count} 個のファイルを削除しました", + "deleteFileTitle": "このファイルを削除しますか?", + "deleteFileBody": "「{name}」は暗号化保存フォルダーから完全に削除されます。", + "deleteFolderTitle": "このフォルダーを削除しますか?", + "deleteFolderBody": "「{dir}」内のすべてのファイルが暗号化保存フォルダーから完全に削除されます。", + "unreadableCount": "保存フォルダー内の読み取れないファイル: {count}", + "unreadableHint": "別のマスターパスワードで暗号化されている、別の保管庫のものである、または破損しています。", + "dismiss": "閉じる", + "viewGrid": "グリッド表示", + "viewList": "リスト表示", + "footerNote": "20 MB までのファイル。Argon2id + XChaCha20-Poly1305 で暗号化され、鍵は保管庫のロック解除中のみメモリー上に保持されます。", + "cancel": "キャンセル", + "save": "保存" } } diff --git a/apps/desktop-ui/messages/ko.json b/apps/desktop-ui/messages/ko.json index 07064145..69e705ba 100644 --- a/apps/desktop-ui/messages/ko.json +++ b/apps/desktop-ui/messages/ko.json @@ -76,7 +76,8 @@ "tokenCounter": "토큰 카운터", "webhookTester": "Webhook 테스터", "websocketTester": "WebSocket 테스터", - "whoisLookup": "Whois 조회" + "whoisLookup": "Whois 조회", + "secureFiles": "파일" }, "Help": { "title": "도움말 및 문서", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "편집", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "복원 중…", "restoreSuccess": "백업이 복원되었습니다", "restoreError": "복원에 실패했습니다 — 잘못된 암호 문구이거나 유효하지 않은 백업 파일입니다" + }, + "backupCodes": { + "title": "백업 코드", + "description": "마스터 비밀번호를 잊었을 때 보관함을 복구하는 일회용 코드입니다.", + "remaining": "{total}개 중 {remaining}개 남음", + "none": "저장된 백업 코드가 없습니다.", + "hint": "새로 생성하면 기존 코드는 모두 무효가 됩니다.", + "passwordPlaceholder": "마스터 비밀번호", + "regenerateButton": "새 코드 생성", + "generating": "생성 중…", + "wrongPassword": "마스터 비밀번호가 올바르지 않습니다", + "success": "새 백업 코드를 생성했습니다", + "error": "백업 코드를 생성하지 못했습니다", + "newCodesWarning": "다시 볼 수 없습니다. 지금 다운로드하거나 복사하세요.", + "downloadButton": "다운로드", + "doneButton": "코드를 저장했습니다" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Whois 조회", "description": "RDAP를 통해 도메인과 IP 등록 정보를 조회합니다." + }, + "secureFiles": { + "title": "파일", + "description": "파일과 폴더를 이름이 가려진 .mydt 객체로 암호화하고, 잠금을 해제한 뒤 탐색·미리 볼 수 있습니다." } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "{docs}개 문서 샘플링 · {fields}개 필드", "colField": "필드", "colTypes": "유형", - "colCoverage": "커버리지" + "colCoverage": "커버리지", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "응답이 클립보드에 복사되었습니다", "codeCopied": "코드가 클립보드에 복사되었습니다", "copyFailed": "클립보드에 복사하지 못했습니다", - "curlCopied": "cURL 명령이 복사되었습니다" + "curlCopied": "cURL 명령이 복사되었습니다", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "컬렉션", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "API 리소스", "newCollection": "새 컬렉션", + "openFolderCollection": "폴더를 컬렉션으로 열기", + "folderCollection": "폴더 컬렉션", "tabCollections": "컬렉션", "tabHistory": "기록", "noCollectionsTitle": "컬렉션 없음", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "새 환경", "variablesHeading": "{name} 변수", "addVariable": "변수 추가", + "exportPostman": "내보내기 (Postman 환경)", "columnVariable": "변수", "columnValue": "값", "placeholderKey": "키", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100% 오프라인" }, "ToolSidebar": { + "expand": "사이드바 펼치기", + "collapse": "사이드바 접기", + "resize": "사이드바 너비 조절", + "resetWidth": "사이드바 너비 초기화", + "moreSections": "섹션 더 보기", + "sections": "사이드바 섹션", "show": "사이드바 표시", "hide": "사이드바 숨기기" }, @@ -5200,5 +5252,92 @@ "accept": "익명으로 공유", "decline": "괜찮습니다" } + }, + "SecureFiles": { + "title": "파일", + "subtitle": "잠금을 해제할 때까지 비공개로 유지되는 파일 — 기기에서 암호화되고 디스크에서는 이름이 가려집니다", + "desktopOnly": "Secure Files는 데스크톱 앱에서 사용할 수 있습니다.", + "allFiles": "모든 파일", + "overview": "개요", + "statFiles": "파일", + "statFolders": "폴더", + "statContent": "콘텐츠 크기", + "statContentHint": "원본 파일의 전체 크기", + "statOnDisk": "디스크 사용량", + "statOverhead": "암호화 오버헤드 {size}", + "byType": "유형별", + "fileCount": "{count, plural, other {파일 #개}}", + "largestFiles": "가장 큰 파일", + "recentlyAdded": "최근 추가됨", + "openContainingFolder": "포함된 폴더 열기", + "types": { + "image": "이미지", + "video": "동영상", + "audio": "오디오", + "pdf": "PDF", + "archive": "압축 파일", + "code": "코드", + "doc": "문서", + "sheet": "스프레드시트", + "file": "기타" + }, + "breadcrumb": "폴더 경로", + "addFiles": "파일 추가", + "addFolder": "폴더 추가", + "importFolder": "폴더 가져오기", + "newFolder": "새 폴더", + "lock": "잠그기", + "settings": "저장 설정", + "storageFolder": "저장 폴더", + "notSet": "설정되지 않음", + "chooseFolder": "폴더 선택", + "changeFolder": "폴더 변경…", + "chooseFolderTitle": "암호화된 파일을 저장할 위치 선택", + "chooseFolderBody": "이 기기나 외장 드라이브의 아무 폴더나 선택하세요. MyDevTools 밖에서는 이름이 가려진 .mydt 파일만 들어 있습니다.", + "changeFolderTitle": "암호화된 파일을 옮길까요?", + "changeFolderBody": "모든 .mydt 파일이 현재 저장 폴더에서 다음에 선택할 폴더로 이동됩니다.", + "folderSet": "저장 폴더를 설정했습니다", + "folderMissingTitle": "저장 폴더를 찾을 수 없음", + "folderMissingBody": "'{dir}'을(를) 사용할 수 없습니다. 연결이 해제된 드라이브에 있거나 다른 컴퓨터에서 복원한 백업의 폴더일 수 있습니다. 다시 연결하거나 새 폴더를 선택하세요.", + "movedCount": "암호화된 파일 {count}개를 옮겼습니다", + "importedCount": "파일 {count}개를 암호화했습니다", + "emptyFolderAdded": "폴더를 추가했습니다 — 암호화할 파일이 없었습니다", + "emptyTitle": "아직 파일이 없습니다", + "emptyBody": "파일이나 폴더 전체를 추가하세요. 각 파일은 임의의 이름을 가진 .mydt 객체로 암호화되며, 원래 이름과 유형, 내용은 여기에서만 보입니다.", + "name": "이름", + "size": "크기", + "modified": "수정일", + "actions": "작업", + "expand": "펼치기", + "collapse": "접기", + "preview": "미리보기", + "noPreview": "이 파일 형식은 미리 볼 수 없습니다 — 내보낸 뒤 다른 앱에서 여세요.", + "export": "내보내기", + "exportWarningTitle": "복호화된 사본을 내보낼까요?", + "exportWarningBody": "선택한 위치에 파일이 암호화되지 않은 상태로 저장됩니다. 그 기기의 무엇이든 읽을 수 있습니다.", + "exported": "복호화된 사본을 내보냈습니다", + "replace": "내용 바꾸기…", + "replaced": "파일 내용을 바꿨습니다", + "rename": "이름 바꾸기", + "fileName": "파일 이름", + "move": "폴더로 이동", + "targetFolder": "폴더", + "targetFolderHint": "중첩 폴더는 슬래시로 구분합니다. 예: project/config. 최상위에 두려면 비워 두세요.", + "folderName": "폴더 이름", + "delete": "삭제", + "deleted": "파일을 삭제했습니다", + "deletedCount": "파일 {count}개를 삭제했습니다", + "deleteFileTitle": "이 파일을 삭제할까요?", + "deleteFileBody": "'{name}'이(가) 암호화된 저장 폴더에서 영구히 삭제됩니다.", + "deleteFolderTitle": "이 폴더를 삭제할까요?", + "deleteFolderBody": "'{dir}' 아래의 모든 파일이 암호화된 저장 폴더에서 영구히 삭제됩니다.", + "unreadableCount": "저장 폴더에서 읽을 수 없는 파일: {count}", + "unreadableHint": "다른 마스터 비밀번호로 암호화되었거나, 다른 보관함에 속하거나, 손상되었습니다.", + "dismiss": "닫기", + "viewGrid": "그리드 보기", + "viewList": "목록 보기", + "footerNote": "최대 20MB 파일. Argon2id + XChaCha20-Poly1305로 암호화되며, 키는 보관함이 잠금 해제된 동안에만 메모리에 유지됩니다.", + "cancel": "취소", + "save": "저장" } } diff --git a/apps/desktop-ui/messages/ms.json b/apps/desktop-ui/messages/ms.json index 5e3138d3..86646f8c 100644 --- a/apps/desktop-ui/messages/ms.json +++ b/apps/desktop-ui/messages/ms.json @@ -76,7 +76,8 @@ "tokenCounter": "Pengira Token", "webhookTester": "Penguji Webhook", "websocketTester": "Penguji WebSocket", - "whoisLookup": "Carian Whois" + "whoisLookup": "Carian Whois", + "secureFiles": "Fail" }, "Help": { "title": "Bantuan & Dokumentasi", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Edit", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Memulihkan…", "restoreSuccess": "Sandaran dipulihkan", "restoreError": "Pemulihan gagal — frasa laluan salah atau fail sandaran tidak sah" + }, + "backupCodes": { + "title": "Kod sandaran", + "description": "Kod sekali guna untuk memulihkan peti besi anda jika terlupa kata laluan induk.", + "remaining": "{remaining} daripada {total} kod berbaki", + "none": "Tiada kod sandaran disimpan.", + "hint": "Menjana set baharu membatalkan semua kod sedia ada.", + "passwordPlaceholder": "Kata laluan induk", + "regenerateButton": "Jana kod baharu", + "generating": "Menjana…", + "wrongPassword": "Kata laluan induk salah", + "success": "Kod sandaran baharu dijana", + "error": "Gagal menjana kod sandaran", + "newCodesWarning": "Anda tidak boleh melihatnya lagi. Muat turun atau salin sekarang.", + "downloadButton": "Muat turun", + "doneButton": "Saya telah simpan kod saya" } }, "Dashboard": { @@ -638,6 +656,10 @@ "whoisLookup": { "title": "Carian Whois", "description": "Cari butiran pendaftaran domain dan IP melalui RDAP." + }, + "secureFiles": { + "title": "Fail", + "description": "Sulitkan fail dan folder menjadi objek .mydt bernama samar; layari dan pratonton selepas dibuka kunci." } }, "tabs": { @@ -2278,7 +2300,8 @@ "responseCopied": "Respons disalin ke papan klip", "codeCopied": "Kod disalin ke papan klip", "copyFailed": "Gagal menyalin ke papan klip", - "curlCopied": "Perintah cURL disalin" + "curlCopied": "Perintah cURL disalin", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Koleksi", @@ -2351,6 +2374,8 @@ "collectionsSidebar": { "title": "Sumber API", "newCollection": "Koleksi baharu", + "openFolderCollection": "Buka folder sebagai koleksi", + "folderCollection": "Koleksi folder", "tabCollections": "Koleksi", "tabHistory": "Sejarah", "noCollectionsTitle": "Tiada koleksi", @@ -2392,6 +2417,7 @@ "placeholderNewEnv": "Persekitaran baharu", "variablesHeading": "Pembolehubah {name}", "addVariable": "Tambah pembolehubah", + "exportPostman": "Eksport (persekitaran Postman)", "columnVariable": "Pembolehubah", "columnValue": "Nilai", "placeholderKey": "Kunci", @@ -2448,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5188,6 +5223,12 @@ "offline": "100% luar talian" }, "ToolSidebar": { + "expand": "Kembangkan bar sisi", + "collapse": "Runtuhkan bar sisi", + "resize": "Ubah saiz bar sisi", + "resetWidth": "Tetapkan semula lebar bar sisi", + "moreSections": "Lagi bahagian", + "sections": "Bahagian bar sisi", "show": "Tunjukkan bar sisi", "hide": "Sembunyikan bar sisi" }, @@ -5211,5 +5252,92 @@ "accept": "Kongsi tanpa nama", "decline": "Tidak, terima kasih" } + }, + "SecureFiles": { + "title": "Fail", + "subtitle": "Fail yang kekal peribadi sehingga anda membukanya — disulitkan pada peranti anda, disamarkan pada cakera", + "desktopOnly": "Secure Files tersedia dalam aplikasi desktop.", + "allFiles": "Semua fail", + "overview": "Gambaran keseluruhan", + "statFiles": "Fail", + "statFolders": "Folder", + "statContent": "Saiz kandungan", + "statContentHint": "Jumlah saiz fail asal", + "statOnDisk": "Pada cakera", + "statOverhead": "{size} overhed penyulitan", + "byType": "Mengikut jenis", + "fileCount": "{count, plural, other {# fail}}", + "largestFiles": "Fail terbesar", + "recentlyAdded": "Baru ditambah", + "openContainingFolder": "Buka folder yang mengandunginya", + "types": { + "image": "Imej", + "video": "Video", + "audio": "Audio", + "pdf": "PDF", + "archive": "Arkib", + "code": "Kod", + "doc": "Dokumen", + "sheet": "Hamparan", + "file": "Lain-lain" + }, + "breadcrumb": "Laluan folder", + "addFiles": "Tambah fail", + "addFolder": "Tambah folder", + "importFolder": "Import folder", + "newFolder": "Folder baharu", + "lock": "Kunci", + "settings": "Tetapan storan", + "storageFolder": "Folder storan", + "notSet": "Belum ditetapkan", + "chooseFolder": "Pilih folder", + "changeFolder": "Tukar folder…", + "chooseFolderTitle": "Pilih tempat fail tersulit disimpan", + "chooseFolderBody": "Pilih mana-mana folder pada peranti ini atau pemacu luaran. Di luar MyDevTools, ia hanya mengandungi fail .mydt bernama samar.", + "changeFolderTitle": "Alihkan fail tersulit?", + "changeFolderBody": "Semua fail .mydt akan dialihkan dari folder storan semasa ke folder yang anda pilih seterusnya.", + "folderSet": "Folder storan ditetapkan", + "folderMissingTitle": "Folder storan tidak dijumpai", + "folderMissingBody": "“{dir}” tidak tersedia — ia mungkin berada pada pemacu yang dicabut atau berasal daripada sandaran yang dipulihkan dari mesin lain. Sambungkan semula, atau pilih folder baharu.", + "movedCount": "{count} fail tersulit dialihkan", + "importedCount": "{count} fail disulitkan", + "emptyFolderAdded": "Folder ditambah — tiada fail untuk disulitkan", + "emptyTitle": "Belum ada fail di sini", + "emptyBody": "Tambah fail atau satu folder penuh. Setiap fail disulitkan menjadi objek .mydt bernama rawak; nama, jenis dan kandungan asal hanya kelihatan di sini.", + "name": "Nama", + "size": "Saiz", + "modified": "Diubah suai", + "actions": "Tindakan", + "expand": "Kembangkan", + "collapse": "Kuncupkan", + "preview": "Pratonton", + "noPreview": "Tiada pratonton untuk jenis fail ini — eksport untuk membukanya dalam aplikasi lain.", + "export": "Eksport", + "exportWarningTitle": "Eksport salinan yang telah dinyahsulit?", + "exportWarningBody": "Fail akan ditulis tanpa sulitan ke lokasi yang anda pilih. Apa-apa pada peranti itu boleh membacanya.", + "exported": "Salinan dinyahsulit dieksport", + "replace": "Ganti kandungan…", + "replaced": "Kandungan fail diganti", + "rename": "Namakan semula", + "fileName": "Nama fail", + "move": "Alihkan ke folder", + "targetFolder": "Folder", + "targetFolderHint": "Gunakan garis miring untuk folder bersarang, contohnya projek/config. Biarkan kosong untuk aras teratas.", + "folderName": "Nama folder", + "delete": "Padam", + "deleted": "Fail dipadam", + "deletedCount": "{count} fail dipadam", + "deleteFileTitle": "Padam fail ini?", + "deleteFileBody": "“{name}” akan dibuang secara kekal daripada folder storan tersulit.", + "deleteFolderTitle": "Padam folder ini?", + "deleteFolderBody": "Setiap fail dalam “{dir}” akan dibuang secara kekal daripada folder storan tersulit.", + "unreadableCount": "Fail tidak boleh dibaca dalam folder storan: {count}", + "unreadableHint": "Ia disulitkan dengan kata laluan induk lain, milik peti besi lain, atau telah rosak.", + "dismiss": "Tutup", + "viewGrid": "Paparan grid", + "viewList": "Paparan senarai", + "footerNote": "Fail sehingga 20 MB. Disulitkan dengan Argon2id + XChaCha20-Poly1305; kunci hanya berada dalam ingatan selagi peti besi dibuka.", + "cancel": "Batal", + "save": "Simpan" } } diff --git a/apps/desktop-ui/messages/nb.json b/apps/desktop-ui/messages/nb.json index 244a3502..a958af60 100644 --- a/apps/desktop-ui/messages/nb.json +++ b/apps/desktop-ui/messages/nb.json @@ -76,7 +76,8 @@ "tokenCounter": "Token-teller", "webhookTester": "Webhook-tester", "websocketTester": "WebSocket-tester", - "whoisLookup": "Whois-oppslag" + "whoisLookup": "Whois-oppslag", + "secureFiles": "Filer" }, "Help": { "title": "Hjelp og dokumentasjon", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Rediger", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Gjenoppretter…", "restoreSuccess": "Sikkerhetskopi gjenopprettet", "restoreError": "Gjenoppretting mislyktes — feil passfrase eller ugyldig fil" + }, + "backupCodes": { + "title": "Sikkerhetskoder", + "description": "Engangskoder som gjenoppretter hvelvet hvis du glemmer hovedpassordet.", + "remaining": "{remaining} av {total} koder igjen", + "none": "Ingen sikkerhetskoder lagret.", + "hint": "Nye koder gjør alle eksisterende koder ugyldige.", + "passwordPlaceholder": "Hovedpassord", + "regenerateButton": "Generer nye koder", + "generating": "Genererer…", + "wrongPassword": "Feil hovedpassord", + "success": "Nye sikkerhetskoder generert", + "error": "Kunne ikke generere sikkerhetskoder", + "newCodesWarning": "Du kan ikke se dem igjen. Last ned eller kopier dem nå.", + "downloadButton": "Last ned", + "doneButton": "Jeg har lagret kodene" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Whois-oppslag", "description": "Slå opp registreringsdetaljer for domene og IP via RDAP." + }, + "secureFiles": { + "title": "Filer", + "description": "Krypter filer og mapper til maskerte .mydt-objekter; bla gjennom og forhåndsvis dem etter opplåsing." } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "Utvalg av {docs} dokumenter · {fields} felter", "colField": "Felt", "colTypes": "Typer", - "colCoverage": "Dekning" + "colCoverage": "Dekning", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "Svar kopiert til utklippstavle", "codeCopied": "Kode kopiert til utklippstavle", "copyFailed": "Kopiering til utklippstavlen mislyktes", - "curlCopied": "cURL-kommando kopiert" + "curlCopied": "cURL-kommando kopiert", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Samlinger", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "API-ressurser", "newCollection": "Ny samling", + "openFolderCollection": "Åpne mappe som samling", + "folderCollection": "Mappesamling", "tabCollections": "Samlinger", "tabHistory": "Historikk", "noCollectionsTitle": "Ingen samlinger", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "Nytt miljø", "variablesHeading": "{name}-variabler", "addVariable": "Legg til variabel", + "exportPostman": "Eksporter (Postman-miljø)", "columnVariable": "Variabel", "columnValue": "Verdi", "placeholderKey": "Nøkkel", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100 % offline" }, "ToolSidebar": { + "expand": "Utvid sidepanel", + "collapse": "Skjul sidepanel", + "resize": "Endre bredde på sidepanel", + "resetWidth": "Tilbakestill bredde", + "moreSections": "Flere seksjoner", + "sections": "Sidepanelseksjoner", "show": "Vis sidepanel", "hide": "Skjul sidepanel" }, @@ -5200,5 +5252,92 @@ "accept": "Del anonymt", "decline": "Nei takk" } + }, + "SecureFiles": { + "title": "Filer", + "subtitle": "Filer som forblir private til du låser dem opp — kryptert på enheten din, maskert på disken", + "desktopOnly": "Secure Files er tilgjengelig i skrivebordsappen.", + "allFiles": "Alle filer", + "overview": "Oversikt", + "statFiles": "Filer", + "statFolders": "Mapper", + "statContent": "Innholdsstørrelse", + "statContentHint": "Samlet størrelse på originalfilene", + "statOnDisk": "På disk", + "statOverhead": "{size} krypteringsoverhead", + "byType": "Etter type", + "fileCount": "{count, plural, one {# fil} other {# filer}}", + "largestFiles": "Største filer", + "recentlyAdded": "Nylig lagt til", + "openContainingFolder": "Åpne overordnet mappe", + "types": { + "image": "Bilder", + "video": "Video", + "audio": "Lyd", + "pdf": "PDF", + "archive": "Arkiver", + "code": "Kode", + "doc": "Dokumenter", + "sheet": "Regneark", + "file": "Annet" + }, + "breadcrumb": "Mappebane", + "addFiles": "Legg til filer", + "addFolder": "Legg til mappe", + "importFolder": "Importer mappe", + "newFolder": "Ny mappe", + "lock": "Lås", + "settings": "Lagringsinnstillinger", + "storageFolder": "Lagringsmappe", + "notSet": "Ikke angitt", + "chooseFolder": "Velg mappe", + "changeFolder": "Bytt mappe…", + "chooseFolderTitle": "Velg hvor krypterte filer lagres", + "chooseFolderBody": "Velg hvilken som helst mappe på denne enheten eller en ekstern disk. Utenfor MyDevTools inneholder den bare maskerte .mydt-filer.", + "changeFolderTitle": "Flytte krypterte filer?", + "changeFolderBody": "Alle .mydt-filer flyttes fra den nåværende lagringsmappen til den du velger nå.", + "folderSet": "Lagringsmappe angitt", + "folderMissingTitle": "Fant ikke lagringsmappen", + "folderMissingBody": "«{dir}» er ikke tilgjengelig — den kan ligge på en frakoblet disk eller stamme fra en gjenopprettet sikkerhetskopi fra en annen maskin. Koble den til igjen, eller velg en ny mappe.", + "movedCount": "Flyttet {count} krypterte filer", + "importedCount": "Krypterte {count} filer", + "emptyFolderAdded": "Mappe lagt til — den inneholdt ingen filer å kryptere", + "emptyTitle": "Ingen filer her ennå", + "emptyBody": "Legg til filer eller en hel mappe. Hver fil krypteres til et .mydt-objekt med tilfeldig navn; opprinnelig navn, type og innhold vises bare her.", + "name": "Navn", + "size": "Størrelse", + "modified": "Endret", + "actions": "Handlinger", + "expand": "Utvid", + "collapse": "Skjul", + "preview": "Forhåndsvisning", + "noPreview": "Ingen forhåndsvisning for denne filtypen — eksporter den for å åpne den i en annen app.", + "export": "Eksporter", + "exportWarningTitle": "Eksportere en dekryptert kopi?", + "exportWarningBody": "Filen skrives ukryptert til stedet du velger. Alt på den enheten kan lese den.", + "exported": "Dekryptert kopi eksportert", + "replace": "Erstatt innhold…", + "replaced": "Filinnholdet ble erstattet", + "rename": "Gi nytt navn", + "fileName": "Filnavn", + "move": "Flytt til mappe", + "targetFolder": "Mappe", + "targetFolderHint": "Bruk skråstreker for nestede mapper, for eksempel prosjekt/config. La feltet stå tomt for øverste nivå.", + "folderName": "Mappenavn", + "delete": "Slett", + "deleted": "Filen ble slettet", + "deletedCount": "Slettet {count} filer", + "deleteFileTitle": "Slette denne filen?", + "deleteFileBody": "«{name}» fjernes permanent fra den krypterte lagringsmappen.", + "deleteFolderTitle": "Slette denne mappen?", + "deleteFolderBody": "Alle filer under «{dir}» fjernes permanent fra den krypterte lagringsmappen.", + "unreadableCount": "Uleselige filer i lagringsmappen: {count}", + "unreadableHint": "De ble kryptert med et annet hovedpassord, hører til et annet hvelv eller er skadet.", + "dismiss": "Lukk", + "viewGrid": "Rutenettvisning", + "viewList": "Listevisning", + "footerNote": "Filer opptil 20 MB. Kryptert med Argon2id + XChaCha20-Poly1305; nøkkelen finnes bare i minnet mens hvelvet er låst opp.", + "cancel": "Avbryt", + "save": "Lagre" } } diff --git a/apps/desktop-ui/messages/nl.json b/apps/desktop-ui/messages/nl.json index 3e94ac0e..b93207a8 100644 --- a/apps/desktop-ui/messages/nl.json +++ b/apps/desktop-ui/messages/nl.json @@ -76,7 +76,8 @@ "tokenCounter": "Token-teller", "webhookTester": "Webhook-tester", "websocketTester": "WebSocket-tester", - "whoisLookup": "Whois-opzoeking" + "whoisLookup": "Whois-opzoeking", + "secureFiles": "Bestanden" }, "Help": { "title": "Help en documentatie", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Bewerken", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Herstellen…", "restoreSuccess": "Back-up hersteld", "restoreError": "Herstellen mislukt — verkeerde wachtwoordzin of ongeldig back-upbestand" + }, + "backupCodes": { + "title": "Back-upcodes", + "description": "Eenmalige codes waarmee je je kluis herstelt als je je hoofdwachtwoord vergeet.", + "remaining": "Nog {remaining} van {total} codes", + "none": "Geen back-upcodes opgeslagen.", + "hint": "Een nieuwe set maakt alle bestaande codes ongeldig.", + "passwordPlaceholder": "Hoofdwachtwoord", + "regenerateButton": "Nieuwe codes genereren", + "generating": "Genereren…", + "wrongPassword": "Onjuist hoofdwachtwoord", + "success": "Nieuwe back-upcodes gegenereerd", + "error": "Back-upcodes genereren mislukt", + "newCodesWarning": "Je kunt ze niet opnieuw bekijken. Download of kopieer ze nu.", + "downloadButton": "Downloaden", + "doneButton": "Ik heb mijn codes opgeslagen" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Whois-opzoeking", "description": "Zoek registratiegegevens van een domein of IP op via RDAP." + }, + "secureFiles": { + "title": "Bestanden", + "description": "Versleutel bestanden en mappen tot gemaskeerde .mydt-objecten; blader en bekijk ze na ontgrendeling." } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "{docs} documenten bemonsterd · {fields} velden", "colField": "Veld", "colTypes": "Typen", - "colCoverage": "Dekking" + "colCoverage": "Dekking", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "Antwoord gekopieerd naar klembord", "codeCopied": "Code gekopieerd naar klembord", "copyFailed": "Kopiëren naar klembord mislukt", - "curlCopied": "cURL-opdracht gekopieerd" + "curlCopied": "cURL-opdracht gekopieerd", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Collecties", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "API-bronnen", "newCollection": "Nieuwe collectie", + "openFolderCollection": "Map openen als collectie", + "folderCollection": "Mapcollectie", "tabCollections": "Collecties", "tabHistory": "Geschiedenis", "noCollectionsTitle": "Geen collecties", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "Nieuwe omgeving", "variablesHeading": "{name} variabelen", "addVariable": "Variabele toevoegen", + "exportPostman": "Exporteren (Postman-omgeving)", "columnVariable": "Variabele", "columnValue": "Waarde", "placeholderKey": "Sleutel", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100% offline" }, "ToolSidebar": { + "expand": "Zijbalk uitklappen", + "collapse": "Zijbalk inklappen", + "resize": "Zijbalkbreedte aanpassen", + "resetWidth": "Breedte herstellen", + "moreSections": "Meer secties", + "sections": "Zijbalksecties", "show": "Zijbalk tonen", "hide": "Zijbalk verbergen" }, @@ -5200,5 +5252,92 @@ "accept": "Anoniem delen", "decline": "Nee, bedankt" } + }, + "SecureFiles": { + "title": "Bestanden", + "subtitle": "Bestanden die privé blijven tot je ze ontgrendelt — versleuteld op je apparaat, gemaskeerd op schijf", + "desktopOnly": "Secure Files is beschikbaar in de desktop-app.", + "allFiles": "Alle bestanden", + "overview": "Overzicht", + "statFiles": "Bestanden", + "statFolders": "Mappen", + "statContent": "Inhoudsgrootte", + "statContentHint": "Totale grootte van de originele bestanden", + "statOnDisk": "Op schijf", + "statOverhead": "{size} versleutelingsoverhead", + "byType": "Op type", + "fileCount": "{count, plural, one {# bestand} other {# bestanden}}", + "largestFiles": "Grootste bestanden", + "recentlyAdded": "Onlangs toegevoegd", + "openContainingFolder": "Bovenliggende map openen", + "types": { + "image": "Afbeeldingen", + "video": "Video", + "audio": "Audio", + "pdf": "PDF", + "archive": "Archieven", + "code": "Code", + "doc": "Documenten", + "sheet": "Spreadsheets", + "file": "Overig" + }, + "breadcrumb": "Mappad", + "addFiles": "Bestanden toevoegen", + "addFolder": "Map toevoegen", + "importFolder": "Map importeren", + "newFolder": "Nieuwe map", + "lock": "Vergrendelen", + "settings": "Opslaginstellingen", + "storageFolder": "Opslagmap", + "notSet": "Niet ingesteld", + "chooseFolder": "Map kiezen", + "changeFolder": "Map wijzigen…", + "chooseFolderTitle": "Kies waar versleutelde bestanden worden opgeslagen", + "chooseFolderBody": "Kies een willekeurige map op dit apparaat of op een externe schijf. Buiten MyDevTools bevat die uitsluitend gemaskeerde .mydt-bestanden.", + "changeFolderTitle": "Versleutelde bestanden verplaatsen?", + "changeFolderBody": "Alle .mydt-bestanden worden van de huidige opslagmap naar de map verplaatst die je hierna kiest.", + "folderSet": "Opslagmap ingesteld", + "folderMissingTitle": "Opslagmap niet gevonden", + "folderMissingBody": "„{dir}” is niet beschikbaar — de map staat mogelijk op een losgekoppelde schijf of komt uit een teruggezette back-up van een andere machine. Sluit de schijf weer aan of kies een nieuwe map.", + "movedCount": "{count} versleutelde bestanden verplaatst", + "importedCount": "{count} bestanden versleuteld", + "emptyFolderAdded": "Map toegevoegd — er waren geen bestanden om te versleutelen", + "emptyTitle": "Hier staan nog geen bestanden", + "emptyBody": "Voeg bestanden of een hele map toe. Elk bestand wordt versleuteld tot een .mydt-object met een willekeurige naam; de originele naam, het type en de inhoud zijn alleen hier zichtbaar.", + "name": "Naam", + "size": "Grootte", + "modified": "Gewijzigd", + "actions": "Acties", + "expand": "Uitvouwen", + "collapse": "Samenvouwen", + "preview": "Voorbeeld", + "noPreview": "Geen voorbeeld voor dit bestandstype — exporteer het om het in een andere app te openen.", + "export": "Exporteren", + "exportWarningTitle": "Een ontsleutelde kopie exporteren?", + "exportWarningBody": "Het bestand wordt onversleuteld naar de gekozen locatie geschreven. Alles op dat apparaat kan het lezen.", + "exported": "Ontsleutelde kopie geëxporteerd", + "replace": "Inhoud vervangen…", + "replaced": "Bestandsinhoud vervangen", + "rename": "Naam wijzigen", + "fileName": "Bestandsnaam", + "move": "Naar map verplaatsen", + "targetFolder": "Map", + "targetFolderHint": "Gebruik schuine strepen voor geneste mappen, bijvoorbeeld project/config. Laat leeg voor het hoogste niveau.", + "folderName": "Mapnaam", + "delete": "Verwijderen", + "deleted": "Bestand verwijderd", + "deletedCount": "{count} bestanden verwijderd", + "deleteFileTitle": "Dit bestand verwijderen?", + "deleteFileBody": "„{name}” wordt definitief uit de versleutelde opslagmap verwijderd.", + "deleteFolderTitle": "Deze map verwijderen?", + "deleteFolderBody": "Elk bestand onder „{dir}” wordt definitief uit de versleutelde opslagmap verwijderd.", + "unreadableCount": "Onleesbare bestanden in de opslagmap: {count}", + "unreadableHint": "Ze zijn versleuteld met een ander hoofdwachtwoord, horen bij een andere kluis of zijn beschadigd.", + "dismiss": "Sluiten", + "viewGrid": "Rasterweergave", + "viewList": "Lijstweergave", + "footerNote": "Bestanden tot 20 MB. Versleuteld met Argon2id + XChaCha20-Poly1305; de sleutel blijft alleen in het geheugen zolang de kluis ontgrendeld is.", + "cancel": "Annuleren", + "save": "Opslaan" } } diff --git a/apps/desktop-ui/messages/pl.json b/apps/desktop-ui/messages/pl.json index 70acbfd6..bdec4d87 100644 --- a/apps/desktop-ui/messages/pl.json +++ b/apps/desktop-ui/messages/pl.json @@ -76,7 +76,8 @@ "tokenCounter": "Licznik tokenów", "webhookTester": "Tester webhooków", "websocketTester": "Tester WebSocket", - "whoisLookup": "Zapytanie Whois" + "whoisLookup": "Zapytanie Whois", + "secureFiles": "Pliki" }, "Help": { "title": "Pomoc i dokumentacja", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Edytuj", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Przywracanie…", "restoreSuccess": "Kopia zapasowa przywrócona", "restoreError": "Przywracanie nie powiodło się — błędne hasło lub nieprawidłowy plik kopii" + }, + "backupCodes": { + "title": "Kody zapasowe", + "description": "Jednorazowe kody przywracające dostęp do sejfu, gdy zapomnisz hasła głównego.", + "remaining": "Pozostało {remaining} z {total} kodów", + "none": "Brak zapisanych kodów zapasowych.", + "hint": "Wygenerowanie nowego zestawu unieważnia wszystkie dotychczasowe kody.", + "passwordPlaceholder": "Hasło główne", + "regenerateButton": "Wygeneruj nowe kody", + "generating": "Generowanie…", + "wrongPassword": "Nieprawidłowe hasło główne", + "success": "Wygenerowano nowe kody zapasowe", + "error": "Nie udało się wygenerować kodów zapasowych", + "newCodesWarning": "Nie zobaczysz ich ponownie. Pobierz je lub skopiuj teraz.", + "downloadButton": "Pobierz", + "doneButton": "Zapisałem kody" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Zapytanie Whois", "description": "Sprawdzaj dane rejestracyjne domen i adresów IP przez RDAP." + }, + "secureFiles": { + "title": "Pliki", + "description": "Szyfruj pliki i foldery do zamaskowanych obiektów .mydt; przeglądaj je po odblokowaniu." } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "Próbkowano {docs} dokumentów · {fields} pól", "colField": "Pole", "colTypes": "Typy", - "colCoverage": "Pokrycie" + "colCoverage": "Pokrycie", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "Response copied to clipboard", "codeCopied": "Code copied to clipboard", "copyFailed": "Kopiowanie do schowka nie powiodło się", - "curlCopied": "Skopiowano polecenie cURL" + "curlCopied": "Skopiowano polecenie cURL", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Collections", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "API resources", "newCollection": "New collection", + "openFolderCollection": "Otwórz folder jako kolekcję", + "folderCollection": "Kolekcja z folderu", "tabCollections": "Collections", "tabHistory": "History", "noCollectionsTitle": "No collections", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "New environment", "variablesHeading": "{name} variables", "addVariable": "Add variable", + "exportPostman": "Eksportuj (środowisko Postman)", "columnVariable": "Variable", "columnValue": "Value", "placeholderKey": "Key", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100% offline" }, "ToolSidebar": { + "expand": "Rozwiń panel boczny", + "collapse": "Zwiń panel boczny", + "resize": "Zmień szerokość panelu", + "resetWidth": "Przywróć szerokość panelu", + "moreSections": "Więcej sekcji", + "sections": "Sekcje panelu bocznego", "show": "Pokaż pasek boczny", "hide": "Ukryj pasek boczny" }, @@ -5200,5 +5252,92 @@ "accept": "Udostępnij anonimowo", "decline": "Nie, dziękuję" } + }, + "SecureFiles": { + "title": "Pliki", + "subtitle": "Pliki pozostają prywatne, dopóki ich nie odblokujesz — zaszyfrowane na urządzeniu, zamaskowane na dysku", + "desktopOnly": "Secure Files jest dostępny w aplikacji desktopowej.", + "allFiles": "Wszystkie pliki", + "overview": "Przegląd", + "statFiles": "Pliki", + "statFolders": "Foldery", + "statContent": "Rozmiar zawartości", + "statContentHint": "Łączny rozmiar oryginalnych plików", + "statOnDisk": "Na dysku", + "statOverhead": "{size} narzutu szyfrowania", + "byType": "Według typu", + "fileCount": "{count, plural, one {# plik} few {# pliki} many {# plików} other {# pliku}}", + "largestFiles": "Największe pliki", + "recentlyAdded": "Ostatnio dodane", + "openContainingFolder": "Otwórz folder nadrzędny", + "types": { + "image": "Obrazy", + "video": "Wideo", + "audio": "Audio", + "pdf": "PDF", + "archive": "Archiwa", + "code": "Kod", + "doc": "Dokumenty", + "sheet": "Arkusze kalkulacyjne", + "file": "Inne" + }, + "breadcrumb": "Ścieżka folderu", + "addFiles": "Dodaj pliki", + "addFolder": "Dodaj folder", + "importFolder": "Importuj folder", + "newFolder": "Nowy folder", + "lock": "Zablokuj", + "settings": "Ustawienia magazynu", + "storageFolder": "Folder magazynu", + "notSet": "Nie ustawiono", + "chooseFolder": "Wybierz folder", + "changeFolder": "Zmień folder…", + "chooseFolderTitle": "Wybierz, gdzie przechowywać zaszyfrowane pliki", + "chooseFolderBody": "Wybierz dowolny folder na tym urządzeniu lub na dysku zewnętrznym. Poza MyDevTools zawiera on wyłącznie zamaskowane pliki .mydt.", + "changeFolderTitle": "Przenieść zaszyfrowane pliki?", + "changeFolderBody": "Wszystkie pliki .mydt zostaną przeniesione z bieżącego folderu magazynu do tego, który wybierzesz teraz.", + "folderSet": "Folder magazynu ustawiony", + "folderMissingTitle": "Nie znaleziono folderu magazynu", + "folderMissingBody": "„{dir}” jest niedostępny — może znajdować się na odłączonym dysku albo pochodzić z kopii zapasowej przywróconej z innego komputera. Podłącz go ponownie lub wybierz nowy folder.", + "movedCount": "Przeniesiono zaszyfrowane pliki: {count}", + "importedCount": "Zaszyfrowano pliki: {count}", + "emptyFolderAdded": "Folder dodany — nie zawierał plików do zaszyfrowania", + "emptyTitle": "Nie ma tu jeszcze plików", + "emptyBody": "Dodaj pliki lub cały folder. Każdy plik jest szyfrowany do obiektu .mydt o losowej nazwie; oryginalna nazwa, typ i zawartość są widoczne tylko tutaj.", + "name": "Nazwa", + "size": "Rozmiar", + "modified": "Zmodyfikowano", + "actions": "Akcje", + "expand": "Rozwiń", + "collapse": "Zwiń", + "preview": "Podgląd", + "noPreview": "Brak podglądu dla tego typu pliku — wyeksportuj go, aby otworzyć w innej aplikacji.", + "export": "Eksportuj", + "exportWarningTitle": "Wyeksportować odszyfrowaną kopię?", + "exportWarningBody": "Plik zostanie zapisany bez szyfrowania w wybranej lokalizacji. Odczyta go wszystko na tym urządzeniu.", + "exported": "Odszyfrowana kopia wyeksportowana", + "replace": "Zastąp zawartość…", + "replaced": "Zawartość pliku zastąpiona", + "rename": "Zmień nazwę", + "fileName": "Nazwa pliku", + "move": "Przenieś do folderu", + "targetFolder": "Folder", + "targetFolderHint": "Użyj ukośników dla folderów zagnieżdżonych, na przykład projekt/config. Pozostaw puste dla najwyższego poziomu.", + "folderName": "Nazwa folderu", + "delete": "Usuń", + "deleted": "Plik usunięty", + "deletedCount": "Usunięto pliki: {count}", + "deleteFileTitle": "Usunąć ten plik?", + "deleteFileBody": "„{name}” zostanie trwale usunięty z zaszyfrowanego folderu magazynu.", + "deleteFolderTitle": "Usunąć ten folder?", + "deleteFolderBody": "Każdy plik w „{dir}” zostanie trwale usunięty z zaszyfrowanego folderu magazynu.", + "unreadableCount": "Nieczytelne pliki w folderze magazynu: {count}", + "unreadableHint": "Zaszyfrowano je innym hasłem głównym, należą do innego sejfu albo są uszkodzone.", + "dismiss": "Odrzuć", + "viewGrid": "Widok siatki", + "viewList": "Widok listy", + "footerNote": "Pliki do 20 MB. Szyfrowanie Argon2id + XChaCha20-Poly1305; klucz pozostaje w pamięci tylko wtedy, gdy sejf jest odblokowany.", + "cancel": "Anuluj", + "save": "Zapisz" } } diff --git a/apps/desktop-ui/messages/pt-BR.json b/apps/desktop-ui/messages/pt-BR.json index 2ad26082..d0afff29 100644 --- a/apps/desktop-ui/messages/pt-BR.json +++ b/apps/desktop-ui/messages/pt-BR.json @@ -76,7 +76,8 @@ "tokenCounter": "Contador de Tokens", "webhookTester": "Testador de Webhook", "websocketTester": "Testador de WebSocket", - "whoisLookup": "Consulta Whois" + "whoisLookup": "Consulta Whois", + "secureFiles": "Arquivos" }, "Help": { "title": "Ajuda e documentação", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Editar", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Restaurando…", "restoreSuccess": "Backup restaurado", "restoreError": "A restauração falhou — senha incorreta ou arquivo de backup inválido" + }, + "backupCodes": { + "title": "Códigos de backup", + "description": "Códigos de uso único que recuperam seu cofre se você esquecer a senha mestra.", + "remaining": "Restam {remaining} de {total} códigos", + "none": "Nenhum código de backup salvo.", + "hint": "Gerar um novo conjunto invalida todos os códigos existentes.", + "passwordPlaceholder": "Senha mestra", + "regenerateButton": "Gerar novos códigos", + "generating": "Gerando…", + "wrongPassword": "Senha mestra incorreta", + "success": "Novos códigos de backup gerados", + "error": "Não foi possível gerar os códigos de backup", + "newCodesWarning": "Você não poderá vê-los novamente. Baixe ou copie agora.", + "downloadButton": "Baixar", + "doneButton": "Salvei meus códigos" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Consulta Whois", "description": "Consulte detalhes de registro de domínio e IP via RDAP." + }, + "secureFiles": { + "title": "Arquivos", + "description": "Criptografe arquivos e pastas em objetos .mydt mascarados; navegue e visualize depois de desbloquear." } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "Amostra de {docs} documentos · {fields} campos", "colField": "Campo", "colTypes": "Tipos", - "colCoverage": "Cobertura" + "colCoverage": "Cobertura", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "Resposta copiada para a área de transferência", "codeCopied": "Código copiado para a área de transferência", "copyFailed": "Falha ao copiar para a área de transferência", - "curlCopied": "Comando cURL copiado" + "curlCopied": "Comando cURL copiado", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Coleções", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "Recursos API", "newCollection": "Nova coleção", + "openFolderCollection": "Abrir pasta como coleção", + "folderCollection": "Coleção de pasta", "tabCollections": "Coleções", "tabHistory": "Histórico", "noCollectionsTitle": "Sem coleções", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "Novo ambiente", "variablesHeading": "Variáveis de {name}", "addVariable": "Adicionar variável", + "exportPostman": "Exportar (ambiente Postman)", "columnVariable": "Variável", "columnValue": "Valor", "placeholderKey": "Chave", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100% offline" }, "ToolSidebar": { + "expand": "Expandir barra lateral", + "collapse": "Recolher barra lateral", + "resize": "Redimensionar barra lateral", + "resetWidth": "Redefinir largura", + "moreSections": "Mais seções", + "sections": "Seções da barra lateral", "show": "Mostrar barra lateral", "hide": "Ocultar barra lateral" }, @@ -5200,5 +5252,92 @@ "accept": "Compartilhar anonimamente", "decline": "Não, obrigado" } + }, + "SecureFiles": { + "title": "Arquivos", + "subtitle": "Arquivos que continuam privados até você desbloqueá-los — criptografados no seu dispositivo, mascarados no disco", + "desktopOnly": "O Secure Files está disponível no aplicativo para desktop.", + "allFiles": "Todos os arquivos", + "overview": "Visão geral", + "statFiles": "Arquivos", + "statFolders": "Pastas", + "statContent": "Tamanho do conteúdo", + "statContentHint": "Tamanho total dos arquivos originais", + "statOnDisk": "Em disco", + "statOverhead": "{size} de sobrecarga de criptografia", + "byType": "Por tipo", + "fileCount": "{count, plural, one {# arquivo} other {# arquivos}}", + "largestFiles": "Maiores arquivos", + "recentlyAdded": "Adicionados recentemente", + "openContainingFolder": "Abrir a pasta que o contém", + "types": { + "image": "Imagens", + "video": "Vídeo", + "audio": "Áudio", + "pdf": "PDF", + "archive": "Arquivos compactados", + "code": "Código", + "doc": "Documentos", + "sheet": "Planilhas", + "file": "Outros" + }, + "breadcrumb": "Caminho da pasta", + "addFiles": "Adicionar arquivos", + "addFolder": "Adicionar pasta", + "importFolder": "Importar pasta", + "newFolder": "Nova pasta", + "lock": "Bloquear", + "settings": "Configurações de armazenamento", + "storageFolder": "Pasta de armazenamento", + "notSet": "Não definida", + "chooseFolder": "Escolher pasta", + "changeFolder": "Mudar pasta…", + "chooseFolderTitle": "Escolha onde os arquivos criptografados ficam guardados", + "chooseFolderBody": "Escolha qualquer pasta neste dispositivo ou em um drive externo. Fora do MyDevTools ela contém apenas arquivos .mydt mascarados.", + "changeFolderTitle": "Mover os arquivos criptografados?", + "changeFolderBody": "Todos os arquivos .mydt serão movidos da pasta de armazenamento atual para a que você escolher a seguir.", + "folderSet": "Pasta de armazenamento definida", + "folderMissingTitle": "Pasta de armazenamento não encontrada", + "folderMissingBody": "«{dir}» não está disponível — ela pode estar em um drive desconectado ou vir de um backup restaurado de outra máquina. Reconecte-a ou escolha uma nova pasta.", + "movedCount": "{count} arquivos criptografados movidos", + "importedCount": "{count} arquivos criptografados", + "emptyFolderAdded": "Pasta adicionada — não havia arquivos para criptografar", + "emptyTitle": "Ainda não há arquivos aqui", + "emptyBody": "Adicione arquivos ou uma pasta inteira. Cada arquivo é criptografado em um objeto .mydt com nome aleatório; o nome, o tipo e o conteúdo originais só aparecem aqui.", + "name": "Nome", + "size": "Tamanho", + "modified": "Modificado", + "actions": "Ações", + "expand": "Expandir", + "collapse": "Recolher", + "preview": "Visualização", + "noPreview": "Sem visualização para este tipo de arquivo — exporte-o para abrir em outro aplicativo.", + "export": "Exportar", + "exportWarningTitle": "Exportar uma cópia descriptografada?", + "exportWarningBody": "O arquivo será gravado sem criptografia no local escolhido. Qualquer coisa nesse dispositivo poderá lê-lo.", + "exported": "Cópia descriptografada exportada", + "replace": "Substituir conteúdo…", + "replaced": "Conteúdo do arquivo substituído", + "rename": "Renomear", + "fileName": "Nome do arquivo", + "move": "Mover para uma pasta", + "targetFolder": "Pasta", + "targetFolderHint": "Use barras para pastas aninhadas, por exemplo projeto/config. Deixe vazio para o nível principal.", + "folderName": "Nome da pasta", + "delete": "Excluir", + "deleted": "Arquivo excluído", + "deletedCount": "{count} arquivos excluídos", + "deleteFileTitle": "Excluir este arquivo?", + "deleteFileBody": "«{name}» será removido permanentemente da pasta de armazenamento criptografada.", + "deleteFolderTitle": "Excluir esta pasta?", + "deleteFolderBody": "Todos os arquivos em «{dir}» serão removidos permanentemente da pasta de armazenamento criptografada.", + "unreadableCount": "Arquivos ilegíveis na pasta de armazenamento: {count}", + "unreadableHint": "Eles foram criptografados com outra senha mestra, pertencem a outro cofre ou estão corrompidos.", + "dismiss": "Dispensar", + "viewGrid": "Visualização em grade", + "viewList": "Visualização em lista", + "footerNote": "Arquivos de até 20 MB. Criptografados com Argon2id + XChaCha20-Poly1305; a chave fica na memória apenas enquanto o cofre está desbloqueado.", + "cancel": "Cancelar", + "save": "Salvar" } } diff --git a/apps/desktop-ui/messages/pt.json b/apps/desktop-ui/messages/pt.json index 83839c88..58a83485 100644 --- a/apps/desktop-ui/messages/pt.json +++ b/apps/desktop-ui/messages/pt.json @@ -76,7 +76,8 @@ "tokenCounter": "Contador de tokens", "webhookTester": "Testador de webhooks", "websocketTester": "Testador de WebSocket", - "whoisLookup": "Pesquisa Whois" + "whoisLookup": "Pesquisa Whois", + "secureFiles": "Ficheiros" }, "Help": { "title": "Ajuda e documentação", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Editar", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "A restaurar…", "restoreSuccess": "Cópia de segurança restaurada", "restoreError": "O restauro falhou — frase de acesso errada ou ficheiro inválido" + }, + "backupCodes": { + "title": "Códigos de recuperação", + "description": "Códigos de utilização única que recuperam o cofre se esquecer a palavra-passe principal.", + "remaining": "Restam {remaining} de {total} códigos", + "none": "Nenhum código de recuperação guardado.", + "hint": "Gerar um novo conjunto invalida todos os códigos existentes.", + "passwordPlaceholder": "Palavra-passe principal", + "regenerateButton": "Gerar novos códigos", + "generating": "A gerar…", + "wrongPassword": "Palavra-passe principal incorreta", + "success": "Novos códigos de recuperação gerados", + "error": "Não foi possível gerar os códigos", + "newCodesWarning": "Não poderá vê-los novamente. Transfira-os ou copie-os agora.", + "downloadButton": "Transferir", + "doneButton": "Já guardei os meus códigos" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Pesquisa Whois", "description": "Consulte os dados de registo de um domínio ou IP através de RDAP." + }, + "secureFiles": { + "title": "Ficheiros", + "description": "Cifre ficheiros e pastas em objetos .mydt mascarados; navegue e pré-visualize depois de desbloquear." } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "Amostra de {docs} documentos · {fields} campos", "colField": "Campo", "colTypes": "Tipos", - "colCoverage": "Cobertura" + "colCoverage": "Cobertura", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "Resposta copiada para a área de transferência", "codeCopied": "Código copiado para a área de transferência", "copyFailed": "Falha ao copiar para a área de transferência", - "curlCopied": "Comando cURL copiado" + "curlCopied": "Comando cURL copiado", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Coleções", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "Recursos API", "newCollection": "Nova coleção", + "openFolderCollection": "Abrir pasta como coleção", + "folderCollection": "Coleção de pasta", "tabCollections": "Coleções", "tabHistory": "Histórico", "noCollectionsTitle": "Sem coleções", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "Novo ambiente", "variablesHeading": "Variáveis de {name}", "addVariable": "Adicionar variável", + "exportPostman": "Exportar (ambiente Postman)", "columnVariable": "Variável", "columnValue": "Valor", "placeholderKey": "Chave", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100% offline" }, "ToolSidebar": { + "expand": "Expandir barra lateral", + "collapse": "Recolher barra lateral", + "resize": "Redimensionar barra lateral", + "resetWidth": "Repor largura", + "moreSections": "Mais secções", + "sections": "Secções da barra lateral", "show": "Mostrar barra lateral", "hide": "Ocultar barra lateral" }, @@ -5200,5 +5252,92 @@ "accept": "Partilhar anonimamente", "decline": "Não, obrigado" } + }, + "SecureFiles": { + "title": "Ficheiros", + "subtitle": "Ficheiros que permanecem privados até os desbloquear — cifrados no seu dispositivo, mascarados no disco", + "desktopOnly": "O Secure Files está disponível na aplicação de ambiente de trabalho.", + "allFiles": "Todos os ficheiros", + "overview": "Visão geral", + "statFiles": "Ficheiros", + "statFolders": "Pastas", + "statContent": "Tamanho do conteúdo", + "statContentHint": "Tamanho total dos ficheiros originais", + "statOnDisk": "Em disco", + "statOverhead": "{size} de sobrecarga de cifragem", + "byType": "Por tipo", + "fileCount": "{count, plural, one {# ficheiro} other {# ficheiros}}", + "largestFiles": "Ficheiros maiores", + "recentlyAdded": "Adicionados recentemente", + "openContainingFolder": "Abrir a pasta que o contém", + "types": { + "image": "Imagens", + "video": "Vídeo", + "audio": "Áudio", + "pdf": "PDF", + "archive": "Arquivos", + "code": "Código", + "doc": "Documentos", + "sheet": "Folhas de cálculo", + "file": "Outros" + }, + "breadcrumb": "Caminho da pasta", + "addFiles": "Adicionar ficheiros", + "addFolder": "Adicionar pasta", + "importFolder": "Importar pasta", + "newFolder": "Nova pasta", + "lock": "Bloquear", + "settings": "Definições de armazenamento", + "storageFolder": "Pasta de armazenamento", + "notSet": "Não definida", + "chooseFolder": "Escolher pasta", + "changeFolder": "Mudar de pasta…", + "chooseFolderTitle": "Escolha onde guardar os ficheiros cifrados", + "chooseFolderBody": "Escolha qualquer pasta neste dispositivo ou numa unidade externa. Fora do MyDevTools contém apenas ficheiros .mydt mascarados.", + "changeFolderTitle": "Mover os ficheiros cifrados?", + "changeFolderBody": "Todos os ficheiros .mydt serão movidos da pasta de armazenamento atual para a que escolher a seguir.", + "folderSet": "Pasta de armazenamento definida", + "folderMissingTitle": "Pasta de armazenamento não encontrada", + "folderMissingBody": "«{dir}» não está disponível — pode estar numa unidade desligada ou pertencer a uma cópia de segurança restaurada de outra máquina. Volte a ligá-la ou escolha uma nova pasta.", + "movedCount": "Movidos {count} ficheiros cifrados", + "importedCount": "Cifrados {count} ficheiros", + "emptyFolderAdded": "Pasta adicionada — não continha ficheiros para cifrar", + "emptyTitle": "Ainda não há ficheiros aqui", + "emptyBody": "Adicione ficheiros ou uma pasta inteira. Cada ficheiro é cifrado num objeto .mydt com nome aleatório; o nome, o tipo e o conteúdo originais só são visíveis aqui.", + "name": "Nome", + "size": "Tamanho", + "modified": "Modificado", + "actions": "Ações", + "expand": "Expandir", + "collapse": "Recolher", + "preview": "Pré-visualização", + "noPreview": "Sem pré-visualização para este tipo de ficheiro — exporte-o para o abrir noutra aplicação.", + "export": "Exportar", + "exportWarningTitle": "Exportar uma cópia decifrada?", + "exportWarningBody": "O ficheiro será escrito sem cifra no local que escolher. Tudo o que estiver nesse dispositivo o poderá ler.", + "exported": "Cópia decifrada exportada", + "replace": "Substituir conteúdo…", + "replaced": "Conteúdo do ficheiro substituído", + "rename": "Mudar o nome", + "fileName": "Nome do ficheiro", + "move": "Mover para uma pasta", + "targetFolder": "Pasta", + "targetFolderHint": "Use barras para pastas aninhadas, por exemplo projeto/config. Deixe vazio para o nível superior.", + "folderName": "Nome da pasta", + "delete": "Eliminar", + "deleted": "Ficheiro eliminado", + "deletedCount": "Eliminados {count} ficheiros", + "deleteFileTitle": "Eliminar este ficheiro?", + "deleteFileBody": "«{name}» será removido permanentemente da pasta de armazenamento cifrada.", + "deleteFolderTitle": "Eliminar esta pasta?", + "deleteFolderBody": "Todos os ficheiros em «{dir}» serão removidos permanentemente da pasta de armazenamento cifrada.", + "unreadableCount": "Ficheiros ilegíveis na pasta de armazenamento: {count}", + "unreadableHint": "Foram cifrados com outra palavra-passe mestra, pertencem a outro cofre ou estão danificados.", + "dismiss": "Dispensar", + "viewGrid": "Vista de grelha", + "viewList": "Vista de lista", + "footerNote": "Ficheiros até 20 MB. Cifrados com Argon2id + XChaCha20-Poly1305; a chave permanece em memória apenas enquanto o cofre está desbloqueado.", + "cancel": "Cancelar", + "save": "Guardar" } } diff --git a/apps/desktop-ui/messages/ru.json b/apps/desktop-ui/messages/ru.json index 342ec3d2..d7459120 100644 --- a/apps/desktop-ui/messages/ru.json +++ b/apps/desktop-ui/messages/ru.json @@ -76,7 +76,8 @@ "tokenCounter": "Счётчик токенов", "webhookTester": "Тестер вебхуков", "websocketTester": "Тестер WebSocket", - "whoisLookup": "Whois-запрос" + "whoisLookup": "Whois-запрос", + "secureFiles": "Файлы" }, "Help": { "title": "Справка и документация", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Изменить", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Восстановление…", "restoreSuccess": "Резервная копия восстановлена", "restoreError": "Восстановление не удалось — неверная фраза или повреждённый файл копии" + }, + "backupCodes": { + "title": "Резервные коды", + "description": "Одноразовые коды для восстановления хранилища, если вы забыли мастер-пароль.", + "remaining": "Осталось {remaining} из {total} кодов", + "none": "Резервные коды не сохранены.", + "hint": "Новый набор делает все прежние коды недействительными.", + "passwordPlaceholder": "Мастер-пароль", + "regenerateButton": "Создать новые коды", + "generating": "Создание…", + "wrongPassword": "Неверный мастер-пароль", + "success": "Новые резервные коды созданы", + "error": "Не удалось создать резервные коды", + "newCodesWarning": "Больше вы их не увидите. Скачайте или скопируйте сейчас.", + "downloadButton": "Скачать", + "doneButton": "Я сохранил коды" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Whois-запрос", "description": "Узнавайте данные регистрации доменов и IP через RDAP." + }, + "secureFiles": { + "title": "Файлы", + "description": "Шифруйте файлы и папки в замаскированные объекты .mydt; просматривайте их после разблокировки." } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "Выборка из {docs} документов · {fields} полей", "colField": "Поле", "colTypes": "Типы", - "colCoverage": "Покрытие" + "colCoverage": "Покрытие", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "Ответ скопирован в буфер обмена", "codeCopied": "Код скопирован в буфер обмена", "copyFailed": "Не удалось скопировать в буфер обмена", - "curlCopied": "Команда cURL скопирована" + "curlCopied": "Команда cURL скопирована", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Коллекции", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "API-ресурсы", "newCollection": "Новая коллекция", + "openFolderCollection": "Открыть папку как коллекцию", + "folderCollection": "Коллекция из папки", "tabCollections": "Коллекции", "tabHistory": "История", "noCollectionsTitle": "Нет коллекций", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "Новое окружение", "variablesHeading": "Переменные {name}", "addVariable": "Добавить переменную", + "exportPostman": "Экспорт (окружение Postman)", "columnVariable": "Переменная", "columnValue": "Значение", "placeholderKey": "Ключ", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100% офлайн" }, "ToolSidebar": { + "expand": "Развернуть боковую панель", + "collapse": "Свернуть боковую панель", + "resize": "Изменить ширину панели", + "resetWidth": "Сбросить ширину панели", + "moreSections": "Ещё разделы", + "sections": "Разделы боковой панели", "show": "Показать боковую панель", "hide": "Скрыть боковую панель" }, @@ -5200,5 +5252,92 @@ "accept": "Делиться анонимно", "decline": "Нет, спасибо" } + }, + "SecureFiles": { + "title": "Файлы", + "subtitle": "Файлы остаются приватными, пока вы их не разблокируете — зашифрованы на устройстве, замаскированы на диске", + "desktopOnly": "Secure Files доступен в настольном приложении.", + "allFiles": "Все файлы", + "overview": "Обзор", + "statFiles": "Файлы", + "statFolders": "Папки", + "statContent": "Размер содержимого", + "statContentHint": "Общий размер исходных файлов", + "statOnDisk": "На диске", + "statOverhead": "{size} накладных расходов шифрования", + "byType": "По типу", + "fileCount": "{count, plural, one {# файл} few {# файла} many {# файлов} other {# файла}}", + "largestFiles": "Самые большие файлы", + "recentlyAdded": "Недавно добавленные", + "openContainingFolder": "Открыть содержащую папку", + "types": { + "image": "Изображения", + "video": "Видео", + "audio": "Аудио", + "pdf": "PDF", + "archive": "Архивы", + "code": "Код", + "doc": "Документы", + "sheet": "Таблицы", + "file": "Прочее" + }, + "breadcrumb": "Путь к папке", + "addFiles": "Добавить файлы", + "addFolder": "Добавить папку", + "importFolder": "Импортировать папку", + "newFolder": "Новая папка", + "lock": "Заблокировать", + "settings": "Настройки хранилища", + "storageFolder": "Папка хранения", + "notSet": "Не задана", + "chooseFolder": "Выбрать папку", + "changeFolder": "Сменить папку…", + "chooseFolderTitle": "Выберите, где хранить зашифрованные файлы", + "chooseFolderBody": "Выберите любую папку на этом устройстве или на внешнем диске. За пределами MyDevTools в ней находятся только замаскированные файлы .mydt.", + "changeFolderTitle": "Переместить зашифрованные файлы?", + "changeFolderBody": "Все файлы .mydt будут перемещены из текущей папки хранения в ту, которую вы выберете далее.", + "folderSet": "Папка хранения задана", + "folderMissingTitle": "Папка хранения не найдена", + "folderMissingBody": "«{dir}» недоступна — возможно, она на отключённом диске или взята из резервной копии с другого компьютера. Подключите диск заново или выберите новую папку.", + "movedCount": "Перемещено зашифрованных файлов: {count}", + "importedCount": "Зашифровано файлов: {count}", + "emptyFolderAdded": "Папка добавлена — в ней не было файлов для шифрования", + "emptyTitle": "Здесь пока нет файлов", + "emptyBody": "Добавьте файлы или целую папку. Каждый файл шифруется в объект .mydt со случайным именем; исходное имя, тип и содержимое видны только здесь.", + "name": "Имя", + "size": "Размер", + "modified": "Изменён", + "actions": "Действия", + "expand": "Развернуть", + "collapse": "Свернуть", + "preview": "Просмотр", + "noPreview": "Для этого типа файла просмотр недоступен — экспортируйте его, чтобы открыть в другом приложении.", + "export": "Экспорт", + "exportWarningTitle": "Экспортировать расшифрованную копию?", + "exportWarningBody": "Файл будет записан в выбранное место без шифрования. Прочитать его сможет что угодно на этом устройстве.", + "exported": "Расшифрованная копия экспортирована", + "replace": "Заменить содержимое…", + "replaced": "Содержимое файла заменено", + "rename": "Переименовать", + "fileName": "Имя файла", + "move": "Переместить в папку", + "targetFolder": "Папка", + "targetFolderHint": "Используйте косые черты для вложенных папок, например проект/config. Оставьте пустым для верхнего уровня.", + "folderName": "Имя папки", + "delete": "Удалить", + "deleted": "Файл удалён", + "deletedCount": "Удалено файлов: {count}", + "deleteFileTitle": "Удалить этот файл?", + "deleteFileBody": "«{name}» будет безвозвратно удалён из зашифрованной папки хранения.", + "deleteFolderTitle": "Удалить эту папку?", + "deleteFolderBody": "Все файлы в «{dir}» будут безвозвратно удалены из зашифрованной папки хранения.", + "unreadableCount": "Нечитаемых файлов в папке хранения: {count}", + "unreadableHint": "Они зашифрованы другим мастер-паролем, принадлежат другому хранилищу или повреждены.", + "dismiss": "Скрыть", + "viewGrid": "Сетка", + "viewList": "Список", + "footerNote": "Файлы до 20 МБ. Шифрование Argon2id + XChaCha20-Poly1305; ключ хранится только в памяти, пока хранилище разблокировано.", + "cancel": "Отмена", + "save": "Сохранить" } } diff --git a/apps/desktop-ui/messages/sv.json b/apps/desktop-ui/messages/sv.json index 2c6845e6..1753ab49 100644 --- a/apps/desktop-ui/messages/sv.json +++ b/apps/desktop-ui/messages/sv.json @@ -76,7 +76,8 @@ "tokenCounter": "Token-räknare", "webhookTester": "Webhook-testare", "websocketTester": "WebSocket-testare", - "whoisLookup": "Whois-uppslag" + "whoisLookup": "Whois-uppslag", + "secureFiles": "Filer" }, "Help": { "title": "Hjälp och dokumentation", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Redigera", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Återställer…", "restoreSuccess": "Säkerhetskopia återställd", "restoreError": "Återställningen misslyckades — fel lösenfras eller ogiltig fil" + }, + "backupCodes": { + "title": "Reservkoder", + "description": "Engångskoder som återställer ditt valv om du glömmer huvudlösenordet.", + "remaining": "{remaining} av {total} koder kvar", + "none": "Inga reservkoder sparade.", + "hint": "Nya koder gör alla befintliga koder ogiltiga.", + "passwordPlaceholder": "Huvudlösenord", + "regenerateButton": "Skapa nya koder", + "generating": "Skapar…", + "wrongPassword": "Fel huvudlösenord", + "success": "Nya reservkoder skapade", + "error": "Kunde inte skapa reservkoder", + "newCodesWarning": "Du kan inte se dem igen. Ladda ner eller kopiera dem nu.", + "downloadButton": "Ladda ner", + "doneButton": "Jag har sparat mina koder" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Whois-uppslag", "description": "Slå upp registreringsuppgifter för domäner och IP via RDAP." + }, + "secureFiles": { + "title": "Filer", + "description": "Kryptera filer och mappar till maskerade .mydt-objekt; bläddra och förhandsgranska dem efter upplåsning." } }, "tabs": { @@ -2278,7 +2300,8 @@ "responseCopied": "Svar kopierat till urklipp", "codeCopied": "Kod kopierad till urklipp", "copyFailed": "Kopiering till urklipp misslyckades", - "curlCopied": "cURL-kommando kopierat" + "curlCopied": "cURL-kommando kopierat", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Samlingar", @@ -2351,6 +2374,8 @@ "collectionsSidebar": { "title": "API-resurser", "newCollection": "Ny samling", + "openFolderCollection": "Öppna mapp som samling", + "folderCollection": "Mappsamling", "tabCollections": "Samlingar", "tabHistory": "Historik", "noCollectionsTitle": "Inga samlingar", @@ -2392,6 +2417,7 @@ "placeholderNewEnv": "Ny miljö", "variablesHeading": "{name}-variabler", "addVariable": "Lägg till variabel", + "exportPostman": "Exportera (Postman-miljö)", "columnVariable": "Variabel", "columnValue": "Värde", "placeholderKey": "Nyckel", @@ -2448,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5188,6 +5223,12 @@ "offline": "100 % offline" }, "ToolSidebar": { + "expand": "Expandera sidopanelen", + "collapse": "Fäll ihop sidopanelen", + "resize": "Ändra sidopanelens bredd", + "resetWidth": "Återställ bredden", + "moreSections": "Fler sektioner", + "sections": "Sidopanelens sektioner", "show": "Visa sidopanel", "hide": "Dölj sidopanel" }, @@ -5211,5 +5252,92 @@ "accept": "Dela anonymt", "decline": "Nej tack" } + }, + "SecureFiles": { + "title": "Filer", + "subtitle": "Filer som förblir privata tills du låser upp dem — krypterade på din enhet, maskerade på disken", + "desktopOnly": "Secure Files finns i skrivbordsappen.", + "allFiles": "Alla filer", + "overview": "Översikt", + "statFiles": "Filer", + "statFolders": "Mappar", + "statContent": "Innehållsstorlek", + "statContentHint": "Total storlek på originalfilerna", + "statOnDisk": "På disk", + "statOverhead": "{size} krypteringsomkostnad", + "byType": "Efter typ", + "fileCount": "{count, plural, one {# fil} other {# filer}}", + "largestFiles": "Största filerna", + "recentlyAdded": "Nyligen tillagda", + "openContainingFolder": "Öppna överordnad mapp", + "types": { + "image": "Bilder", + "video": "Video", + "audio": "Ljud", + "pdf": "PDF", + "archive": "Arkiv", + "code": "Kod", + "doc": "Dokument", + "sheet": "Kalkylblad", + "file": "Övrigt" + }, + "breadcrumb": "Mappsökväg", + "addFiles": "Lägg till filer", + "addFolder": "Lägg till mapp", + "importFolder": "Importera mapp", + "newFolder": "Ny mapp", + "lock": "Lås", + "settings": "Lagringsinställningar", + "storageFolder": "Lagringsmapp", + "notSet": "Inte angiven", + "chooseFolder": "Välj mapp", + "changeFolder": "Byt mapp…", + "chooseFolderTitle": "Välj var krypterade filer ska lagras", + "chooseFolderBody": "Välj vilken mapp som helst på den här enheten eller på en extern disk. Utanför MyDevTools innehåller den bara maskerade .mydt-filer.", + "changeFolderTitle": "Flytta krypterade filer?", + "changeFolderBody": "Alla .mydt-filer flyttas från den nuvarande lagringsmappen till den du väljer härnäst.", + "folderSet": "Lagringsmapp angiven", + "folderMissingTitle": "Lagringsmappen hittades inte", + "folderMissingBody": "”{dir}” är inte tillgänglig — den kan ligga på en frånkopplad disk eller komma från en återställd säkerhetskopia från en annan dator. Anslut den igen eller välj en ny mapp.", + "movedCount": "Flyttade {count} krypterade filer", + "importedCount": "Krypterade {count} filer", + "emptyFolderAdded": "Mappen lades till — den innehöll inga filer att kryptera", + "emptyTitle": "Inga filer här ännu", + "emptyBody": "Lägg till filer eller en hel mapp. Varje fil krypteras till ett .mydt-objekt med slumpmässigt namn; originalnamn, typ och innehåll syns bara här.", + "name": "Namn", + "size": "Storlek", + "modified": "Ändrad", + "actions": "Åtgärder", + "expand": "Expandera", + "collapse": "Fäll ihop", + "preview": "Förhandsgranska", + "noPreview": "Ingen förhandsgranskning för den här filtypen — exportera den för att öppna den i en annan app.", + "export": "Exportera", + "exportWarningTitle": "Exportera en dekrypterad kopia?", + "exportWarningBody": "Filen skrivs okrypterad till platsen du väljer. Allt på den enheten kan läsa den.", + "exported": "Dekrypterad kopia exporterad", + "replace": "Ersätt innehåll…", + "replaced": "Filinnehållet ersattes", + "rename": "Byt namn", + "fileName": "Filnamn", + "move": "Flytta till mapp", + "targetFolder": "Mapp", + "targetFolderHint": "Använd snedstreck för nästlade mappar, till exempel projekt/config. Lämna tomt för toppnivån.", + "folderName": "Mappnamn", + "delete": "Ta bort", + "deleted": "Filen togs bort", + "deletedCount": "Tog bort {count} filer", + "deleteFileTitle": "Ta bort den här filen?", + "deleteFileBody": "”{name}” tas bort permanent från den krypterade lagringsmappen.", + "deleteFolderTitle": "Ta bort den här mappen?", + "deleteFolderBody": "Alla filer under ”{dir}” tas bort permanent från den krypterade lagringsmappen.", + "unreadableCount": "Oläsbara filer i lagringsmappen: {count}", + "unreadableHint": "De krypterades med ett annat huvudlösenord, hör till ett annat valv eller är skadade.", + "dismiss": "Avfärda", + "viewGrid": "Rutnätsvy", + "viewList": "Listvy", + "footerNote": "Filer upp till 20 MB. Krypterade med Argon2id + XChaCha20-Poly1305; nyckeln finns bara i minnet medan valvet är upplåst.", + "cancel": "Avbryt", + "save": "Spara" } } diff --git a/apps/desktop-ui/messages/tr.json b/apps/desktop-ui/messages/tr.json index 7d9d6106..a4c0333b 100644 --- a/apps/desktop-ui/messages/tr.json +++ b/apps/desktop-ui/messages/tr.json @@ -76,7 +76,8 @@ "tokenCounter": "Token Sayacı", "webhookTester": "Webhook Test Aracı", "websocketTester": "WebSocket Test Aracı", - "whoisLookup": "Whois Sorgulama" + "whoisLookup": "Whois Sorgulama", + "secureFiles": "Dosyalar" }, "Help": { "title": "Yardım ve Dokümantasyon", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Düzenle", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Geri yükleniyor…", "restoreSuccess": "Yedek geri yüklendi", "restoreError": "Geri yükleme başarısız — yanlış parola veya geçersiz yedek dosyası" + }, + "backupCodes": { + "title": "Yedek kodlar", + "description": "Ana parolanızı unutursanız kasanızı kurtaran tek kullanımlık kodlar.", + "remaining": "{total} koddan {remaining} tanesi kaldı", + "none": "Kayıtlı yedek kod yok.", + "hint": "Yeni kod üretmek mevcut tüm kodları geçersiz kılar.", + "passwordPlaceholder": "Ana parola", + "regenerateButton": "Yeni kod üret", + "generating": "Üretiliyor…", + "wrongPassword": "Ana parola yanlış", + "success": "Yeni yedek kodlar üretildi", + "error": "Yedek kodlar üretilemedi", + "newCodesWarning": "Bunları tekrar göremezsiniz. Şimdi indirin veya kopyalayın.", + "downloadButton": "İndir", + "doneButton": "Kodlarımı kaydettim" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Whois Sorgulama", "description": "Alan adı ve IP kayıt bilgilerini RDAP üzerinden sorgulayın." + }, + "secureFiles": { + "title": "Dosyalar", + "description": "Dosyaları ve klasörleri maskelenmiş .mydt nesnelerine şifreleyin; kilidi açtıktan sonra göz atın ve önizleyin." } }, "tabs": { @@ -2278,7 +2300,8 @@ "responseCopied": "Yanıt panoya kopyalandı", "codeCopied": "Kod panoya kopyalandı", "copyFailed": "Panoya kopyalanamadı", - "curlCopied": "cURL komutu kopyalandı" + "curlCopied": "cURL komutu kopyalandı", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Koleksiyonlar", @@ -2351,6 +2374,8 @@ "collectionsSidebar": { "title": "API kaynakları", "newCollection": "Yeni koleksiyon", + "openFolderCollection": "Klasörü koleksiyon olarak aç", + "folderCollection": "Klasör koleksiyonu", "tabCollections": "Koleksiyonlar", "tabHistory": "Geçmiş", "noCollectionsTitle": "Koleksiyon yok", @@ -2392,6 +2417,7 @@ "placeholderNewEnv": "Yeni ortam", "variablesHeading": "{name} değişkenleri", "addVariable": "Değişken ekle", + "exportPostman": "Dışa aktar (Postman ortamı)", "columnVariable": "Değişken", "columnValue": "Değer", "placeholderKey": "Anahtar", @@ -2448,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5188,6 +5223,12 @@ "offline": "%100 çevrimdışı" }, "ToolSidebar": { + "expand": "Kenar çubuğunu genişlet", + "collapse": "Kenar çubuğunu daralt", + "resize": "Kenar çubuğunu yeniden boyutlandır", + "resetWidth": "Genişliği sıfırla", + "moreSections": "Daha fazla bölüm", + "sections": "Kenar çubuğu bölümleri", "show": "Kenar çubuğunu göster", "hide": "Kenar çubuğunu gizle" }, @@ -5211,5 +5252,92 @@ "accept": "Anonim olarak paylaş", "decline": "Hayır, teşekkürler" } + }, + "SecureFiles": { + "title": "Dosyalar", + "subtitle": "Kilidini açana kadar gizli kalan dosyalar — cihazınızda şifrelenir, diskte maskelenir", + "desktopOnly": "Secure Files masaüstü uygulamasında kullanılabilir.", + "allFiles": "Tüm dosyalar", + "overview": "Genel bakış", + "statFiles": "Dosyalar", + "statFolders": "Klasörler", + "statContent": "İçerik boyutu", + "statContentHint": "Orijinal dosyaların toplam boyutu", + "statOnDisk": "Diskte", + "statOverhead": "{size} şifreleme ek yükü", + "byType": "Türe göre", + "fileCount": "{count, plural, other {# dosya}}", + "largestFiles": "En büyük dosyalar", + "recentlyAdded": "Son eklenenler", + "openContainingFolder": "Bulunduğu klasörü aç", + "types": { + "image": "Görseller", + "video": "Video", + "audio": "Ses", + "pdf": "PDF", + "archive": "Arşivler", + "code": "Kod", + "doc": "Belgeler", + "sheet": "Elektronik tablolar", + "file": "Diğer" + }, + "breadcrumb": "Klasör yolu", + "addFiles": "Dosya ekle", + "addFolder": "Klasör ekle", + "importFolder": "Klasör içe aktar", + "newFolder": "Yeni klasör", + "lock": "Kilitle", + "settings": "Depolama ayarları", + "storageFolder": "Depolama klasörü", + "notSet": "Ayarlanmadı", + "chooseFolder": "Klasör seç", + "changeFolder": "Klasörü değiştir…", + "chooseFolderTitle": "Şifreli dosyaların nerede saklanacağını seçin", + "chooseFolderBody": "Bu cihazdaki veya harici bir sürücüdeki herhangi bir klasörü seçin. MyDevTools dışında yalnızca maskelenmiş .mydt dosyaları içerir.", + "changeFolderTitle": "Şifreli dosyalar taşınsın mı?", + "changeFolderBody": "Tüm .mydt dosyaları geçerli depolama klasöründen birazdan seçeceğiniz klasöre taşınacak.", + "folderSet": "Depolama klasörü ayarlandı", + "folderMissingTitle": "Depolama klasörü bulunamadı", + "folderMissingBody": "“{dir}” kullanılamıyor — çıkarılmış bir sürücüde olabilir ya da başka bir makineden geri yüklenen bir yedeğe ait olabilir. Yeniden bağlayın veya yeni bir klasör seçin.", + "movedCount": "{count} şifreli dosya taşındı", + "importedCount": "{count} dosya şifrelendi", + "emptyFolderAdded": "Klasör eklendi — şifrelenecek dosya yoktu", + "emptyTitle": "Burada henüz dosya yok", + "emptyBody": "Dosya ya da bütün bir klasör ekleyin. Her dosya rastgele adlı bir .mydt nesnesine şifrelenir; orijinal ad, tür ve içerik yalnızca burada görünür.", + "name": "Ad", + "size": "Boyut", + "modified": "Değiştirilme", + "actions": "İşlemler", + "expand": "Genişlet", + "collapse": "Daralt", + "preview": "Önizleme", + "noPreview": "Bu dosya türü için önizleme yok — başka bir uygulamada açmak için dışa aktarın.", + "export": "Dışa aktar", + "exportWarningTitle": "Şifresi çözülmüş bir kopya dışa aktarılsın mı?", + "exportWarningBody": "Dosya, seçtiğiniz konuma şifrelenmeden yazılacak. O cihazdaki her şey onu okuyabilir.", + "exported": "Şifresi çözülmüş kopya dışa aktarıldı", + "replace": "İçeriği değiştir…", + "replaced": "Dosya içeriği değiştirildi", + "rename": "Yeniden adlandır", + "fileName": "Dosya adı", + "move": "Klasöre taşı", + "targetFolder": "Klasör", + "targetFolderHint": "İç içe klasörler için eğik çizgi kullanın, örneğin proje/config. En üst düzey için boş bırakın.", + "folderName": "Klasör adı", + "delete": "Sil", + "deleted": "Dosya silindi", + "deletedCount": "{count} dosya silindi", + "deleteFileTitle": "Bu dosya silinsin mi?", + "deleteFileBody": "“{name}” şifreli depolama klasöründen kalıcı olarak kaldırılacak.", + "deleteFolderTitle": "Bu klasör silinsin mi?", + "deleteFolderBody": "“{dir}” altındaki tüm dosyalar şifreli depolama klasöründen kalıcı olarak kaldırılacak.", + "unreadableCount": "Depolama klasöründe okunamayan dosyalar: {count}", + "unreadableHint": "Farklı bir ana parolayla şifrelenmiş, başka bir kasaya ait ya da bozulmuş olabilirler.", + "dismiss": "Kapat", + "viewGrid": "Izgara görünümü", + "viewList": "Liste görünümü", + "footerNote": "20 MB'a kadar dosyalar. Argon2id + XChaCha20-Poly1305 ile şifrelenir; anahtar yalnızca kasa açıkken bellekte tutulur.", + "cancel": "İptal", + "save": "Kaydet" } } diff --git a/apps/desktop-ui/messages/uk.json b/apps/desktop-ui/messages/uk.json index d9b4fcf7..2820a96c 100644 --- a/apps/desktop-ui/messages/uk.json +++ b/apps/desktop-ui/messages/uk.json @@ -76,7 +76,8 @@ "tokenCounter": "Лічильник токенів", "webhookTester": "Тестер вебхуків", "websocketTester": "Тестер WebSocket", - "whoisLookup": "Whois-запит" + "whoisLookup": "Whois-запит", + "secureFiles": "Файли" }, "Help": { "title": "Довідка та документація", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Змінити", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Відновлення…", "restoreSuccess": "Резервну копію відновлено", "restoreError": "Відновлення не вдалося — неправильна фраза або недійсний файл копії" + }, + "backupCodes": { + "title": "Резервні коди", + "description": "Одноразові коди для відновлення сховища, якщо ви забули майстер-пароль.", + "remaining": "Залишилося {remaining} з {total} кодів", + "none": "Резервні коди не збережено.", + "hint": "Новий набір робить усі попередні коди недійсними.", + "passwordPlaceholder": "Майстер-пароль", + "regenerateButton": "Створити нові коди", + "generating": "Створення…", + "wrongPassword": "Невірний майстер-пароль", + "success": "Нові резервні коди створено", + "error": "Не вдалося створити резервні коди", + "newCodesWarning": "Ви більше їх не побачите. Завантажте або скопіюйте зараз.", + "downloadButton": "Завантажити", + "doneButton": "Я зберіг коди" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Whois-запит", "description": "Дізнавайтеся дані реєстрації доменів та IP через RDAP." + }, + "secureFiles": { + "title": "Файли", + "description": "Шифруйте файли та теки в замасковані об'єкти .mydt; переглядайте їх після розблокування." } }, "tabs": { @@ -2278,7 +2300,8 @@ "responseCopied": "Відповідь скопійовано до буфера обміну", "codeCopied": "Код скопійовано до буфера обміну", "copyFailed": "Не вдалося скопіювати до буфера обміну", - "curlCopied": "Команду cURL скопійовано" + "curlCopied": "Команду cURL скопійовано", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Колекції", @@ -2351,6 +2374,8 @@ "collectionsSidebar": { "title": "API ресурси", "newCollection": "Нова колекція", + "openFolderCollection": "Відкрити папку як колекцію", + "folderCollection": "Колекція з папки", "tabCollections": "Колекції", "tabHistory": "Історія", "noCollectionsTitle": "Немає колекцій", @@ -2392,6 +2417,7 @@ "placeholderNewEnv": "Нове середовище", "variablesHeading": "Змінні {name}", "addVariable": "Додати змінну", + "exportPostman": "Експорт (середовище Postman)", "columnVariable": "Змінна", "columnValue": "Значення", "placeholderKey": "Ключ", @@ -2448,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5188,6 +5223,12 @@ "offline": "100% офлайн" }, "ToolSidebar": { + "expand": "Розгорнути бічну панель", + "collapse": "Згорнути бічну панель", + "resize": "Змінити ширину панелі", + "resetWidth": "Скинути ширину панелі", + "moreSections": "Більше розділів", + "sections": "Розділи бічної панелі", "show": "Показати бічну панель", "hide": "Сховати бічну панель" }, @@ -5211,5 +5252,92 @@ "accept": "Ділитися анонімно", "decline": "Ні, дякую" } + }, + "SecureFiles": { + "title": "Файли", + "subtitle": "Файли залишаються приватними, доки ви їх не розблокуєте — зашифровані на пристрої, замасковані на диску", + "desktopOnly": "Secure Files доступний у настільному застосунку.", + "allFiles": "Усі файли", + "overview": "Огляд", + "statFiles": "Файли", + "statFolders": "Теки", + "statContent": "Розмір вмісту", + "statContentHint": "Загальний розмір вихідних файлів", + "statOnDisk": "На диску", + "statOverhead": "{size} накладних витрат шифрування", + "byType": "За типом", + "fileCount": "{count, plural, one {# файл} few {# файли} many {# файлів} other {# файла}}", + "largestFiles": "Найбільші файли", + "recentlyAdded": "Нещодавно додані", + "openContainingFolder": "Відкрити теку, що містить файл", + "types": { + "image": "Зображення", + "video": "Відео", + "audio": "Аудіо", + "pdf": "PDF", + "archive": "Архіви", + "code": "Код", + "doc": "Документи", + "sheet": "Таблиці", + "file": "Інше" + }, + "breadcrumb": "Шлях до теки", + "addFiles": "Додати файли", + "addFolder": "Додати теку", + "importFolder": "Імпортувати теку", + "newFolder": "Нова тека", + "lock": "Заблокувати", + "settings": "Налаштування сховища", + "storageFolder": "Тека сховища", + "notSet": "Не задано", + "chooseFolder": "Вибрати теку", + "changeFolder": "Змінити теку…", + "chooseFolderTitle": "Виберіть, де зберігати зашифровані файли", + "chooseFolderBody": "Виберіть будь-яку теку на цьому пристрої або на зовнішньому диску. Поза MyDevTools вона містить лише замасковані файли .mydt.", + "changeFolderTitle": "Перемістити зашифровані файли?", + "changeFolderBody": "Усі файли .mydt буде переміщено з поточної теки сховища до тієї, яку ви виберете далі.", + "folderSet": "Теку сховища задано", + "folderMissingTitle": "Теку сховища не знайдено", + "folderMissingBody": "«{dir}» недоступна — можливо, вона на від'єднаному диску або походить із відновленої резервної копії з іншого комп'ютера. Під'єднайте його знову або виберіть нову теку.", + "movedCount": "Переміщено зашифрованих файлів: {count}", + "importedCount": "Зашифровано файлів: {count}", + "emptyFolderAdded": "Теку додано — у ній не було файлів для шифрування", + "emptyTitle": "Тут ще немає файлів", + "emptyBody": "Додайте файли або цілу теку. Кожен файл шифрується в об'єкт .mydt з випадковим іменем; вихідне ім'я, тип і вміст видно лише тут.", + "name": "Ім'я", + "size": "Розмір", + "modified": "Змінено", + "actions": "Дії", + "expand": "Розгорнути", + "collapse": "Згорнути", + "preview": "Перегляд", + "noPreview": "Для цього типу файлу перегляд недоступний — експортуйте його, щоб відкрити в іншому застосунку.", + "export": "Експортувати", + "exportWarningTitle": "Експортувати розшифровану копію?", + "exportWarningBody": "Файл буде записано у вибране місце без шифрування. Прочитати його зможе будь-що на цьому пристрої.", + "exported": "Розшифровану копію експортовано", + "replace": "Замінити вміст…", + "replaced": "Вміст файлу замінено", + "rename": "Перейменувати", + "fileName": "Ім'я файлу", + "move": "Перемістити до теки", + "targetFolder": "Тека", + "targetFolderHint": "Використовуйте похилі риски для вкладених тек, наприклад проєкт/config. Залиште порожнім для верхнього рівня.", + "folderName": "Ім'я теки", + "delete": "Видалити", + "deleted": "Файл видалено", + "deletedCount": "Видалено файлів: {count}", + "deleteFileTitle": "Видалити цей файл?", + "deleteFileBody": "«{name}» буде остаточно видалено із зашифрованої теки сховища.", + "deleteFolderTitle": "Видалити цю теку?", + "deleteFolderBody": "Усі файли в «{dir}» буде остаточно видалено із зашифрованої теки сховища.", + "unreadableCount": "Нечитабельних файлів у теці сховища: {count}", + "unreadableHint": "Їх зашифровано іншим головним паролем, вони належать до іншого сховища або пошкоджені.", + "dismiss": "Сховати", + "viewGrid": "Сітка", + "viewList": "Список", + "footerNote": "Файли до 20 МБ. Шифрування Argon2id + XChaCha20-Poly1305; ключ зберігається лише в пам'яті, доки сховище розблоковане.", + "cancel": "Скасувати", + "save": "Зберегти" } } diff --git a/apps/desktop-ui/messages/vi.json b/apps/desktop-ui/messages/vi.json index bca53f10..344f364c 100644 --- a/apps/desktop-ui/messages/vi.json +++ b/apps/desktop-ui/messages/vi.json @@ -76,7 +76,8 @@ "tokenCounter": "Bộ đếm token", "webhookTester": "Trình kiểm tra Webhook", "websocketTester": "Trình kiểm tra WebSocket", - "whoisLookup": "Tra cứu Whois" + "whoisLookup": "Tra cứu Whois", + "secureFiles": "Tệp" }, "Help": { "title": "Trợ giúp & Tài liệu", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "Chỉnh sửa", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "Đang khôi phục…", "restoreSuccess": "Đã khôi phục bản sao lưu", "restoreError": "Khôi phục thất bại — sai cụm mật khẩu hoặc tệp sao lưu không hợp lệ" + }, + "backupCodes": { + "title": "Mã dự phòng", + "description": "Mã dùng một lần để khôi phục kho lưu trữ nếu bạn quên mật khẩu chính.", + "remaining": "Còn {remaining} trong {total} mã", + "none": "Chưa lưu mã dự phòng nào.", + "hint": "Tạo bộ mã mới sẽ vô hiệu hóa tất cả mã hiện có.", + "passwordPlaceholder": "Mật khẩu chính", + "regenerateButton": "Tạo mã mới", + "generating": "Đang tạo…", + "wrongPassword": "Mật khẩu chính không đúng", + "success": "Đã tạo mã dự phòng mới", + "error": "Không thể tạo mã dự phòng", + "newCodesWarning": "Bạn sẽ không xem lại được. Hãy tải xuống hoặc sao chép ngay.", + "downloadButton": "Tải xuống", + "doneButton": "Tôi đã lưu mã" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Tra cứu Whois", "description": "Tra cứu thông tin đăng ký tên miền và IP qua RDAP." + }, + "secureFiles": { + "title": "Tệp", + "description": "Mã hóa tệp và thư mục thành các đối tượng .mydt được che tên; duyệt và xem trước sau khi mở khóa." } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "Đã lấy mẫu {docs} tài liệu · {fields} trường", "colField": "Trường", "colTypes": "Loại", - "colCoverage": "Độ phủ" + "colCoverage": "Độ phủ", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "Đã sao chép phản hồi vào bộ nhớ tạm", "codeCopied": "Đã sao chép mã vào bộ nhớ tạm", "copyFailed": "Sao chép vào clipboard không thành công", - "curlCopied": "Đã sao chép lệnh cURL" + "curlCopied": "Đã sao chép lệnh cURL", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "Bộ sưu tập", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "Tài nguyên API", "newCollection": "Bộ sưu tập mới", + "openFolderCollection": "Mở thư mục dưới dạng bộ sưu tập", + "folderCollection": "Bộ sưu tập thư mục", "tabCollections": "Bộ sưu tập", "tabHistory": "Lịch sử", "noCollectionsTitle": "Chưa có bộ sưu tập", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "Môi trường mới", "variablesHeading": "Biến {name}", "addVariable": "Thêm biến", + "exportPostman": "Xuất (môi trường Postman)", "columnVariable": "Biến", "columnValue": "Giá trị", "placeholderKey": "Khóa", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100% ngoại tuyến" }, "ToolSidebar": { + "expand": "Mở rộng thanh bên", + "collapse": "Thu gọn thanh bên", + "resize": "Thay đổi kích thước thanh bên", + "resetWidth": "Đặt lại chiều rộng thanh bên", + "moreSections": "Thêm mục", + "sections": "Các mục thanh bên", "show": "Hiện thanh bên", "hide": "Ẩn thanh bên" }, @@ -5200,5 +5252,92 @@ "accept": "Chia sẻ ẩn danh", "decline": "Không, cảm ơn" } + }, + "SecureFiles": { + "title": "Tệp", + "subtitle": "Những tệp luôn riêng tư cho đến khi bạn mở khóa — mã hóa trên thiết bị, che tên trên ổ đĩa", + "desktopOnly": "Secure Files có sẵn trong ứng dụng máy tính.", + "allFiles": "Tất cả tệp", + "overview": "Tổng quan", + "statFiles": "Tệp", + "statFolders": "Thư mục", + "statContent": "Dung lượng nội dung", + "statContentHint": "Tổng dung lượng của các tệp gốc", + "statOnDisk": "Trên ổ đĩa", + "statOverhead": "{size} chi phí mã hóa", + "byType": "Theo loại", + "fileCount": "{count, plural, other {# tệp}}", + "largestFiles": "Tệp lớn nhất", + "recentlyAdded": "Mới thêm gần đây", + "openContainingFolder": "Mở thư mục chứa tệp", + "types": { + "image": "Hình ảnh", + "video": "Video", + "audio": "Âm thanh", + "pdf": "PDF", + "archive": "Tệp nén", + "code": "Mã nguồn", + "doc": "Tài liệu", + "sheet": "Bảng tính", + "file": "Khác" + }, + "breadcrumb": "Đường dẫn thư mục", + "addFiles": "Thêm tệp", + "addFolder": "Thêm thư mục", + "importFolder": "Nhập thư mục", + "newFolder": "Thư mục mới", + "lock": "Khóa", + "settings": "Cài đặt lưu trữ", + "storageFolder": "Thư mục lưu trữ", + "notSet": "Chưa đặt", + "chooseFolder": "Chọn thư mục", + "changeFolder": "Đổi thư mục…", + "chooseFolderTitle": "Chọn nơi lưu các tệp đã mã hóa", + "chooseFolderBody": "Chọn bất kỳ thư mục nào trên thiết bị này hoặc trên ổ đĩa ngoài. Bên ngoài MyDevTools, thư mục đó chỉ chứa các tệp .mydt đã được che tên.", + "changeFolderTitle": "Di chuyển các tệp đã mã hóa?", + "changeFolderBody": "Tất cả tệp .mydt sẽ được chuyển từ thư mục lưu trữ hiện tại sang thư mục bạn chọn tiếp theo.", + "folderSet": "Đã đặt thư mục lưu trữ", + "folderMissingTitle": "Không tìm thấy thư mục lưu trữ", + "folderMissingBody": "“{dir}” không khả dụng — có thể nó nằm trên ổ đĩa đã tháo hoặc thuộc về một bản sao lưu khôi phục từ máy khác. Hãy kết nối lại hoặc chọn thư mục mới.", + "movedCount": "Đã chuyển {count} tệp đã mã hóa", + "importedCount": "Đã mã hóa {count} tệp", + "emptyFolderAdded": "Đã thêm thư mục — không có tệp nào để mã hóa", + "emptyTitle": "Chưa có tệp nào ở đây", + "emptyBody": "Thêm tệp hoặc cả một thư mục. Mỗi tệp được mã hóa thành một đối tượng .mydt có tên ngẫu nhiên; tên, loại và nội dung gốc chỉ hiển thị ở đây.", + "name": "Tên", + "size": "Kích thước", + "modified": "Đã sửa đổi", + "actions": "Thao tác", + "expand": "Mở rộng", + "collapse": "Thu gọn", + "preview": "Xem trước", + "noPreview": "Không xem trước được loại tệp này — hãy xuất ra để mở bằng ứng dụng khác.", + "export": "Xuất", + "exportWarningTitle": "Xuất một bản sao đã giải mã?", + "exportWarningBody": "Tệp sẽ được ghi ở dạng không mã hóa tại vị trí bạn chọn. Mọi thứ trên thiết bị đó đều có thể đọc được.", + "exported": "Đã xuất bản sao đã giải mã", + "replace": "Thay nội dung…", + "replaced": "Đã thay nội dung tệp", + "rename": "Đổi tên", + "fileName": "Tên tệp", + "move": "Chuyển vào thư mục", + "targetFolder": "Thư mục", + "targetFolderHint": "Dùng dấu gạch chéo cho thư mục lồng nhau, ví dụ project/config. Để trống nếu muốn ở cấp cao nhất.", + "folderName": "Tên thư mục", + "delete": "Xóa", + "deleted": "Đã xóa tệp", + "deletedCount": "Đã xóa {count} tệp", + "deleteFileTitle": "Xóa tệp này?", + "deleteFileBody": "“{name}” sẽ bị xóa vĩnh viễn khỏi thư mục lưu trữ đã mã hóa.", + "deleteFolderTitle": "Xóa thư mục này?", + "deleteFolderBody": "Mọi tệp trong “{dir}” sẽ bị xóa vĩnh viễn khỏi thư mục lưu trữ đã mã hóa.", + "unreadableCount": "Tệp không đọc được trong thư mục lưu trữ: {count}", + "unreadableHint": "Chúng được mã hóa bằng mật khẩu chính khác, thuộc về một kho khác, hoặc đã hỏng.", + "dismiss": "Bỏ qua", + "viewGrid": "Dạng lưới", + "viewList": "Dạng danh sách", + "footerNote": "Tệp tối đa 20 MB. Mã hóa bằng Argon2id + XChaCha20-Poly1305; khóa chỉ nằm trong bộ nhớ khi kho đang mở khóa.", + "cancel": "Hủy", + "save": "Lưu" } } diff --git a/apps/desktop-ui/messages/zh.json b/apps/desktop-ui/messages/zh.json index 2a5e15b1..461e2643 100644 --- a/apps/desktop-ui/messages/zh.json +++ b/apps/desktop-ui/messages/zh.json @@ -76,7 +76,8 @@ "tokenCounter": "Token 计数器", "webhookTester": "Webhook 测试器", "websocketTester": "WebSocket 测试器", - "whoisLookup": "Whois 查询" + "whoisLookup": "Whois 查询", + "secureFiles": "文件" }, "Help": { "title": "帮助与文档", @@ -183,6 +184,7 @@ "namePlaceholder": "e.g. Alex", "avatarLabel": "Avatar URL", "avatarPlaceholder": "https://…", + "edit": "编辑", "save": "Save", "saved": "Profile saved", "saveError": "Could not save your profile" @@ -307,6 +309,22 @@ "restoring": "正在恢复…", "restoreSuccess": "备份已恢复", "restoreError": "恢复失败 — 口令错误或备份文件无效" + }, + "backupCodes": { + "title": "备用代码", + "description": "一次性代码,可在忘记主密码时恢复保险库。", + "remaining": "剩余 {remaining} / {total} 个代码", + "none": "尚未保存备用代码。", + "hint": "生成新代码会使现有代码全部失效。", + "passwordPlaceholder": "主密码", + "regenerateButton": "生成新代码", + "generating": "生成中…", + "wrongPassword": "主密码不正确", + "success": "已生成新的备用代码", + "error": "无法生成备用代码", + "newCodesWarning": "离开后将无法再次查看,请立即下载或复制。", + "downloadButton": "下载", + "doneButton": "我已保存代码" } }, "Dashboard": { @@ -635,6 +653,10 @@ "whoisLookup": { "title": "Whois 查询", "description": "通过 RDAP 查询域名和 IP 的注册信息。" + }, + "secureFiles": { + "title": "文件", + "description": "将文件和文件夹加密为名称已掩码的 .mydt 对象;解锁后即可浏览和预览。" } }, "tabs": { @@ -1926,7 +1948,18 @@ "sampled": "已抽样 {docs} 个文档 · {fields} 个字段", "colField": "字段", "colTypes": "类型", - "colCoverage": "覆盖率" + "colCoverage": "覆盖率", + "sampleRandom": "Random", + "sampleFirst": "First", + "sampleLast": "Last", + "sampleAll": "All", + "sampleSizeN": "{n} docs", + "allCapped": "capped at 10k", + "validatorTitle": "Validation rules", + "exportSchemaBtn": "Export", + "exportJsonSchema": "JSON Schema", + "exportMongoose": "Mongoose model", + "exportHtml": "HTML report" } }, "DataExplorer": { @@ -2267,7 +2300,8 @@ "responseCopied": "响应已复制到剪贴板", "codeCopied": "代码已复制到剪贴板", "copyFailed": "复制到剪贴板失败", - "curlCopied": "已复制 cURL 命令" + "curlCopied": "已复制 cURL 命令", + "curlNoUrl": "No URL found in that cURL command" }, "layout": { "collections": "集合", @@ -2340,6 +2374,8 @@ "collectionsSidebar": { "title": "API 资源", "newCollection": "新建集合", + "openFolderCollection": "将文件夹作为集合打开", + "folderCollection": "文件夹集合", "tabCollections": "集合", "tabHistory": "历史", "noCollectionsTitle": "暂无集合", @@ -2381,6 +2417,7 @@ "placeholderNewEnv": "新环境", "variablesHeading": "{name} 变量", "addVariable": "添加变量", + "exportPostman": "导出(Postman 环境)", "columnVariable": "变量", "columnValue": "值", "placeholderKey": "键", @@ -2437,7 +2474,16 @@ "save": "Save request", "importCurl": "Import cURL", "environments": "Environments", - "shortcuts": "Keyboard shortcuts" + "shortcuts": "Keyboard shortcuts", + "importCollection": "Import collection (Postman / HAR)", + "cookies": "Cookies", + "perfRun": "Performance run", + "fuzzRun": "Fuzz run", + "publicMocks": "Public mocks", + "plugins": "Plugins", + "metrics": "Metrics", + "recorder": "Capture & replay", + "extensionImported": "Imported {method} {url} from extension" } }, "RichEditor": { @@ -5177,6 +5223,12 @@ "offline": "100% 离线" }, "ToolSidebar": { + "expand": "展开侧边栏", + "collapse": "折叠侧边栏", + "resize": "调整侧边栏宽度", + "resetWidth": "重置侧边栏宽度", + "moreSections": "更多分区", + "sections": "侧边栏分区", "show": "显示侧边栏", "hide": "隐藏侧边栏" }, @@ -5200,5 +5252,92 @@ "accept": "匿名共享", "decline": "不用了" } + }, + "SecureFiles": { + "title": "文件", + "subtitle": "在你解锁之前始终保持私密的文件 — 在设备上加密,在磁盘上掩码", + "desktopOnly": "Secure Files 可在桌面应用中使用。", + "allFiles": "全部文件", + "overview": "概览", + "statFiles": "文件", + "statFolders": "文件夹", + "statContent": "内容大小", + "statContentHint": "原始文件的总大小", + "statOnDisk": "磁盘占用", + "statOverhead": "加密开销 {size}", + "byType": "按类型", + "fileCount": "{count, plural, other {# 个文件}}", + "largestFiles": "最大的文件", + "recentlyAdded": "最近添加", + "openContainingFolder": "打开所在文件夹", + "types": { + "image": "图片", + "video": "视频", + "audio": "音频", + "pdf": "PDF", + "archive": "压缩包", + "code": "代码", + "doc": "文档", + "sheet": "电子表格", + "file": "其他" + }, + "breadcrumb": "文件夹路径", + "addFiles": "添加文件", + "addFolder": "添加文件夹", + "importFolder": "导入文件夹", + "newFolder": "新建文件夹", + "lock": "锁定", + "settings": "存储设置", + "storageFolder": "存储文件夹", + "notSet": "未设置", + "chooseFolder": "选择文件夹", + "changeFolder": "更改文件夹…", + "chooseFolderTitle": "选择加密文件的存放位置", + "chooseFolderBody": "在本设备或外接驱动器上任选一个文件夹。在 MyDevTools 之外,它只包含名称已掩码的 .mydt 文件。", + "changeFolderTitle": "移动加密文件?", + "changeFolderBody": "所有 .mydt 文件都会从当前存储文件夹移动到你接下来选择的文件夹。", + "folderSet": "已设置存储文件夹", + "folderMissingTitle": "未找到存储文件夹", + "folderMissingBody": "「{dir}」不可用 — 它可能位于已拔出的驱动器上,或来自从其他电脑恢复的备份。请重新连接,或选择新的文件夹。", + "movedCount": "已移动 {count} 个加密文件", + "importedCount": "已加密 {count} 个文件", + "emptyFolderAdded": "已添加文件夹 — 其中没有可加密的文件", + "emptyTitle": "这里还没有文件", + "emptyBody": "添加文件或整个文件夹。每个文件都会被加密成名称随机的 .mydt 对象;原始名称、类型和内容只在这里可见。", + "name": "名称", + "size": "大小", + "modified": "修改时间", + "actions": "操作", + "expand": "展开", + "collapse": "折叠", + "preview": "预览", + "noPreview": "此类型的文件无法预览 — 导出后用其他应用打开。", + "export": "导出", + "exportWarningTitle": "导出解密副本?", + "exportWarningBody": "文件将以未加密的形式写入你选择的位置。该设备上的任何程序都能读取它。", + "exported": "已导出解密副本", + "replace": "替换内容…", + "replaced": "已替换文件内容", + "rename": "重命名", + "fileName": "文件名", + "move": "移动到文件夹", + "targetFolder": "文件夹", + "targetFolderHint": "嵌套文件夹用斜杠分隔,例如 project/config。留空表示顶层。", + "folderName": "文件夹名称", + "delete": "删除", + "deleted": "已删除文件", + "deletedCount": "已删除 {count} 个文件", + "deleteFileTitle": "删除此文件?", + "deleteFileBody": "「{name}」将从加密存储文件夹中永久删除。", + "deleteFolderTitle": "删除此文件夹?", + "deleteFolderBody": "「{dir}」下的所有文件都将从加密存储文件夹中永久删除。", + "unreadableCount": "存储文件夹中无法读取的文件:{count}", + "unreadableHint": "它们使用了其他主密码加密、属于其他保险库,或者已损坏。", + "dismiss": "关闭", + "viewGrid": "网格视图", + "viewList": "列表视图", + "footerNote": "文件上限 20 MB。使用 Argon2id + XChaCha20-Poly1305 加密;密钥仅在保险库解锁期间保存在内存中。", + "cancel": "取消", + "save": "保存" } } diff --git a/apps/desktop-ui/package.json b/apps/desktop-ui/package.json index 2f02c1d5..b6c4339f 100644 --- a/apps/desktop-ui/package.json +++ b/apps/desktop-ui/package.json @@ -53,6 +53,8 @@ "@tanstack/react-virtual": "^3.14.10", "@tauri-apps/api": "^2.11.1", "@tauri-apps/plugin-deep-link": "^2.4.9", + "@tauri-apps/plugin-dialog": "^2.7.2", + "@tauri-apps/plugin-fs": "^2.4.0", "@tauri-apps/plugin-opener": "^2.5.4", "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-updater": "^2.10.1", @@ -91,7 +93,7 @@ "lucide-react": "^0.552.0", "marked": "^18.0.10", "mime-types": "^3.0.2", - "monaco-editor": "^0.53.0", + "monaco-editor": "^0.55.1", "mongodb": "^7.5.0", "mysql2": "^3.23.4", "netmask": "^2.1.1", diff --git a/apps/desktop-ui/scripts/add-tool-sidebar-i18n.mjs b/apps/desktop-ui/scripts/add-tool-sidebar-i18n.mjs new file mode 100644 index 00000000..24ed9cf2 --- /dev/null +++ b/apps/desktop-ui/scripts/add-tool-sidebar-i18n.mjs @@ -0,0 +1,48 @@ +// Adds the collapse-to-icon rail keys to the ToolSidebar namespace in every +// locale. Idempotent: existing keys are left untouched. +import { readFileSync, writeFileSync, readdirSync } from 'node:fs' +import { join } from 'node:path' + +const DIR = new URL('../messages/', import.meta.url).pathname + +const T = { + en: { expand: 'Expand sidebar', collapse: 'Collapse sidebar', resize: 'Resize sidebar', resetWidth: 'Reset sidebar width', moreSections: 'More sections', sections: 'Sidebar sections' }, + af: { expand: 'Vou sybalk uit', collapse: 'Vou sybalk in', resize: 'Verstel sybalk se grootte', resetWidth: 'Herstel sybalk se breedte', moreSections: 'Meer afdelings', sections: 'Sybalk-afdelings' }, + ar: { expand: 'توسيع الشريط الجانبي', collapse: 'طي الشريط الجانبي', resize: 'تغيير حجم الشريط الجانبي', resetWidth: 'إعادة تعيين عرض الشريط الجانبي', moreSections: 'المزيد من الأقسام', sections: 'أقسام الشريط الجانبي' }, + ca: { expand: 'Amplia la barra lateral', collapse: 'Redueix la barra lateral', resize: 'Redimensiona la barra lateral', resetWidth: "Restableix l'amplada de la barra lateral", moreSections: 'Més seccions', sections: 'Seccions de la barra lateral' }, + cs: { expand: 'Rozbalit postranní panel', collapse: 'Sbalit postranní panel', resize: 'Změnit šířku panelu', resetWidth: 'Obnovit šířku panelu', moreSections: 'Další sekce', sections: 'Sekce postranního panelu' }, + da: { expand: 'Udvid sidepanel', collapse: 'Skjul sidepanel', resize: 'Tilpas sidepanelets bredde', resetWidth: 'Nulstil sidepanelets bredde', moreSections: 'Flere sektioner', sections: 'Sidepanelsektioner' }, + de: { expand: 'Seitenleiste ausklappen', collapse: 'Seitenleiste einklappen', resize: 'Seitenleiste anpassen', resetWidth: 'Breite zurücksetzen', moreSections: 'Weitere Abschnitte', sections: 'Abschnitte der Seitenleiste' }, + el: { expand: 'Ανάπτυξη πλαϊνής στήλης', collapse: 'Σύμπτυξη πλαϊνής στήλης', resize: 'Αλλαγή πλάτους', resetWidth: 'Επαναφορά πλάτους', moreSections: 'Περισσότερες ενότητες', sections: 'Ενότητες πλαϊνής στήλης' }, + es: { expand: 'Expandir barra lateral', collapse: 'Contraer barra lateral', resize: 'Redimensionar barra lateral', resetWidth: 'Restablecer ancho', moreSections: 'Más secciones', sections: 'Secciones de la barra lateral' }, + fa: { expand: 'گسترش نوار کناری', collapse: 'جمع کردن نوار کناری', resize: 'تغییر اندازه نوار کناری', resetWidth: 'بازنشانی عرض نوار کناری', moreSections: 'بخش‌های بیشتر', sections: 'بخش‌های نوار کناری' }, + fr: { expand: 'Développer la barre latérale', collapse: 'Réduire la barre latérale', resize: 'Redimensionner la barre latérale', resetWidth: 'Réinitialiser la largeur', moreSections: 'Plus de sections', sections: 'Sections de la barre latérale' }, + id: { expand: 'Bentangkan bilah sisi', collapse: 'Ciutkan bilah sisi', resize: 'Ubah ukuran bilah sisi', resetWidth: 'Atur ulang lebar bilah sisi', moreSections: 'Bagian lainnya', sections: 'Bagian bilah sisi' }, + it: { expand: 'Espandi barra laterale', collapse: 'Comprimi barra laterale', resize: 'Ridimensiona barra laterale', resetWidth: 'Reimposta larghezza', moreSections: 'Altre sezioni', sections: 'Sezioni della barra laterale' }, + ja: { expand: 'サイドバーを展開', collapse: 'サイドバーを折りたたむ', resize: 'サイドバーの幅を変更', resetWidth: 'サイドバーの幅をリセット', moreSections: 'その他のセクション', sections: 'サイドバーのセクション' }, + ko: { expand: '사이드바 펼치기', collapse: '사이드바 접기', resize: '사이드바 너비 조절', resetWidth: '사이드바 너비 초기화', moreSections: '섹션 더 보기', sections: '사이드바 섹션' }, + ms: { expand: 'Kembangkan bar sisi', collapse: 'Runtuhkan bar sisi', resize: 'Ubah saiz bar sisi', resetWidth: 'Tetapkan semula lebar bar sisi', moreSections: 'Lagi bahagian', sections: 'Bahagian bar sisi' }, + nb: { expand: 'Utvid sidepanel', collapse: 'Skjul sidepanel', resize: 'Endre bredde på sidepanel', resetWidth: 'Tilbakestill bredde', moreSections: 'Flere seksjoner', sections: 'Sidepanelseksjoner' }, + nl: { expand: 'Zijbalk uitklappen', collapse: 'Zijbalk inklappen', resize: 'Zijbalkbreedte aanpassen', resetWidth: 'Breedte herstellen', moreSections: 'Meer secties', sections: 'Zijbalksecties' }, + pl: { expand: 'Rozwiń panel boczny', collapse: 'Zwiń panel boczny', resize: 'Zmień szerokość panelu', resetWidth: 'Przywróć szerokość panelu', moreSections: 'Więcej sekcji', sections: 'Sekcje panelu bocznego' }, + 'pt-BR': { expand: 'Expandir barra lateral', collapse: 'Recolher barra lateral', resize: 'Redimensionar barra lateral', resetWidth: 'Redefinir largura', moreSections: 'Mais seções', sections: 'Seções da barra lateral' }, + pt: { expand: 'Expandir barra lateral', collapse: 'Recolher barra lateral', resize: 'Redimensionar barra lateral', resetWidth: 'Repor largura', moreSections: 'Mais secções', sections: 'Secções da barra lateral' }, + ru: { expand: 'Развернуть боковую панель', collapse: 'Свернуть боковую панель', resize: 'Изменить ширину панели', resetWidth: 'Сбросить ширину панели', moreSections: 'Ещё разделы', sections: 'Разделы боковой панели' }, + sv: { expand: 'Expandera sidopanelen', collapse: 'Fäll ihop sidopanelen', resize: 'Ändra sidopanelens bredd', resetWidth: 'Återställ bredden', moreSections: 'Fler sektioner', sections: 'Sidopanelens sektioner' }, + tr: { expand: 'Kenar çubuğunu genişlet', collapse: 'Kenar çubuğunu daralt', resize: 'Kenar çubuğunu yeniden boyutlandır', resetWidth: 'Genişliği sıfırla', moreSections: 'Daha fazla bölüm', sections: 'Kenar çubuğu bölümleri' }, + uk: { expand: 'Розгорнути бічну панель', collapse: 'Згорнути бічну панель', resize: 'Змінити ширину панелі', resetWidth: 'Скинути ширину панелі', moreSections: 'Більше розділів', sections: 'Розділи бічної панелі' }, + vi: { expand: 'Mở rộng thanh bên', collapse: 'Thu gọn thanh bên', resize: 'Thay đổi kích thước thanh bên', resetWidth: 'Đặt lại chiều rộng thanh bên', moreSections: 'Thêm mục', sections: 'Các mục thanh bên' }, + zh: { expand: '展开侧边栏', collapse: '折叠侧边栏', resize: '调整侧边栏宽度', resetWidth: '重置侧边栏宽度', moreSections: '更多分区', sections: '侧边栏分区' }, +} + +let changed = 0 +for (const file of readdirSync(DIR).filter((f) => f.endsWith('.json'))) { + const locale = file.replace(/\.json$/, '') + const add = T[locale] ?? T.en + const path = join(DIR, file) + const json = JSON.parse(readFileSync(path, 'utf8')) + json.ToolSidebar = { ...add, ...(json.ToolSidebar ?? {}) } + writeFileSync(path, JSON.stringify(json, null, 2) + '\n') + changed++ +} +console.log(`updated ${changed} locale files`) diff --git a/apps/desktop-ui/scripts/build-tauri.mjs b/apps/desktop-ui/scripts/build-tauri.mjs index 29b3534e..849f261e 100644 --- a/apps/desktop-ui/scripts/build-tauri.mjs +++ b/apps/desktop-ui/scripts/build-tauri.mjs @@ -10,7 +10,6 @@ * Output: apps/desktop-ui/out/ */ import { execSync, spawnSync } from "node:child_process"; -import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -26,27 +25,6 @@ if (major < 20 || (major === 20 && minor < 19)) { process.exit(1); } -// Activation is mandatory: a desktop build without real Firebase credentials -// produces an app nobody can activate. Hard-fail rather than ship it. -// Next loads .env files itself; this check just guards CI/misconfigured shells. -const hasFirebaseKey = - !!process.env.NEXT_PUBLIC_FIREBASE_API_KEY || - [".env", ".env.local", ".env.production"].some((f) => { - try { - return fs.readFileSync(path.join(uiRoot, f), "utf8").includes("NEXT_PUBLIC_FIREBASE_API_KEY="); - } catch { - return false; - } - }); -if (!hasFirebaseKey) { - console.error( - "build-tauri: NEXT_PUBLIC_FIREBASE_API_KEY is not set (env or .env file). Desktop " + - "activation requires real Firebase credentials baked into the bundle — export the " + - "NEXT_PUBLIC_FIREBASE_* vars before building." - ); - process.exit(1); -} - const env = { ...process.env, TAURI_BUILD: "1", diff --git a/apps/desktop-ui/src/app/app/notes/notes-client-layout.tsx b/apps/desktop-ui/src/app/app/notes/notes-client-layout.tsx index 94cc182a..cfb8220b 100644 --- a/apps/desktop-ui/src/app/app/notes/notes-client-layout.tsx +++ b/apps/desktop-ui/src/app/app/notes/notes-client-layout.tsx @@ -5,6 +5,9 @@ import NotesSidebar from "@/components/notes/NotesSidebar"; import { ToolSidebarLayout } from "@/components/tools/tool-sidebar"; import { NotebookPen } from "lucide-react"; import { useTranslations } from "next-intl"; +import { useVaultGuard } from "@/hooks/use-vault-guard"; +import { VaultLockedPlaceholder } from "@/components/vault-locked-placeholder"; +import { VaultRestoringSkeleton } from "@/components/vault-restoring-skeleton"; function NotesLayout({ children }: { children: React.ReactNode }) { const tSidebar = useTranslations("Notes.sidebar"); @@ -33,6 +36,20 @@ function NotesLayout({ children }: { children: React.ReactNode }) { } export default function NotesClientLayout({ children }: { children: React.ReactNode }) { + const { isUnlocked, isRestoring } = useVaultGuard(); + + // Gate here, not in page.tsx. The page is this layout's *children*, so a gate + // there swaps the editor for the placeholder while the sidebar column keeps + // rendering — a note tree, a search field and a New note button, all backed + // by a vault that cannot be read. password-manager, api-keys and + // environment-manager gate before their ToolSidebarLayout for the same + // reason; notes only differed because its layout is a separate wrapper. + // + // Above NotesProvider, so a locked vault does not mount the provider and + // have it fail to load notes it has no key for. + if (isRestoring) return ; + if (!isUnlocked) return ; + return ( {children} diff --git a/apps/desktop-ui/src/app/app/secure-files/layout.tsx b/apps/desktop-ui/src/app/app/secure-files/layout.tsx new file mode 100644 index 00000000..2c33eefe --- /dev/null +++ b/apps/desktop-ui/src/app/app/secure-files/layout.tsx @@ -0,0 +1,7 @@ +import { generateToolMetadata } from '@/lib/metadata' + +export const metadata = generateToolMetadata('secure-files') + +export default function SecureFilesLayout({ children }: { children: React.ReactNode }) { + return <>{children} +} diff --git a/apps/desktop-ui/src/app/app/secure-files/page.tsx b/apps/desktop-ui/src/app/app/secure-files/page.tsx new file mode 100644 index 00000000..85748677 --- /dev/null +++ b/apps/desktop-ui/src/app/app/secure-files/page.tsx @@ -0,0 +1,20 @@ +"use client"; +import { useTranslations } from "next-intl"; +import { SecureFilesTool } from "@/components/secure-files/secure-files-tool"; +import { useVaultGuard } from "@/hooks/use-vault-guard"; +import { VaultLockedPlaceholder } from "@/components/vault-locked-placeholder"; +import { VaultRestoringSkeleton } from "@/components/vault-restoring-skeleton"; +import { isDesktop } from "@/lib/desktop/is-desktop"; + +export default function SecureFilesPage() { + const t = useTranslations("SecureFiles"); + // Files are encrypted with a key derived from the master password; the + // Rust side only holds that key while the vault is unlocked. + const { isUnlocked, isRestoring } = useVaultGuard(); + if (!isDesktop()) { + return

{t("desktopOnly")}

; + } + if (isRestoring) return ; + if (!isUnlocked) return ; + return ; +} diff --git a/apps/desktop-ui/src/app/dashboard/page.tsx b/apps/desktop-ui/src/app/dashboard/page.tsx index 6df8dc43..ceec1f1b 100644 --- a/apps/desktop-ui/src/app/dashboard/page.tsx +++ b/apps/desktop-ui/src/app/dashboard/page.tsx @@ -262,7 +262,6 @@ const DashboardPage: React.FC = () => {
{/* ── Mobile Sticky Header (outside padded container for full-bleed) ── */} { {/* ── Desktop Hero (inside padded container for alignment) ── */} diff --git a/apps/desktop-ui/src/app/settings/page.tsx b/apps/desktop-ui/src/app/settings/page.tsx index 1a38bcde..1da11fed 100644 --- a/apps/desktop-ui/src/app/settings/page.tsx +++ b/apps/desktop-ui/src/app/settings/page.tsx @@ -4,7 +4,9 @@ import React, { useState, useEffect } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { Sun, Globe, Palette, Lock, Pipette } from 'lucide-react' +import { Sun, Moon, Monitor, Globe, Palette, Lock, Pipette, User, Archive } from 'lucide-react' +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { IDLE_TIMEOUT_KEY, getIdleTimeoutMinutes } from '@/lib/use-idle-lock' import { Switch } from '@/components/ui/switch' import { getVaultIconsEnabled, setVaultIconsEnabled } from '@/lib/vault-icon-pref' @@ -19,6 +21,7 @@ import { ProfileCard } from '@/components/settings/profile-card' import { AppVersionLabel } from '@/components/desktop/app-version-label' import { FactoryResetCard } from '@/components/desktop/factory-reset-card' import { BackupRestoreCard } from '@/components/desktop/backup-restore-card' +import { BackupCodesCard } from '@/components/settings/backup-codes-card' import { useActiveWorkspace } from '@/store/workspace-store' import { Briefcase } from 'lucide-react' @@ -75,10 +78,31 @@ export default function SettingsPage() {

{t('subtitle')}

-
+ + + + + {t('userProfile.title')} + + + + {t('appearance.title')} + + + + {t('security.title')} + + + + {t('backup.title')} + + - + + + + @@ -100,16 +124,25 @@ export default function SettingsPage() {
- + {/* Single-select toggles deselect on re-click: ignore the empty value + so there is always an active theme. */} + value && setTheme(value)} + className="justify-start" + > + + + + + + + + + +
@@ -177,10 +210,53 @@ export default function SettingsPage() {
+ +
+
+ + +
+ +
+ + @@ -247,68 +323,15 @@ export default function SettingsPage() { - - - - - - - {t('language.title')} - - - {t('language.description')} - - - -
- - -
-

- {t('language.helpText')} -

-
-
- - + +
- - - - + + + + + + ) } diff --git a/apps/desktop-ui/src/components/api-client/__tests__/tabs-context.test.tsx b/apps/desktop-ui/src/components/api-client/__tests__/tabs-context.test.tsx index bde4ceba..1d3dfe80 100644 --- a/apps/desktop-ui/src/components/api-client/__tests__/tabs-context.test.tsx +++ b/apps/desktop-ui/src/components/api-client/__tests__/tabs-context.test.tsx @@ -83,3 +83,34 @@ describe("guard functions are exported", () => { expect(typeof useTabsActions).toBe("function") }) }) + +describe("slimTabsForStorage", () => { + const { createNewTab, slimTabsForStorage } = require("../context/tabs-context") + const fat = () => { + const t = createNewTab() + t.response = { status: 200, statusText: "OK", headers: {}, body: "x".repeat(1000), time: 1, size: 1 } + t.isLoading = true + t.graphqlSchema = { types: [] } + t.examples = [{ id: "e", name: "ex", capturedAt: 1, request: { method: "GET", url: "u", headers: [], body: t.body }, response: { status: 200, statusText: "OK", headers: {}, body: "big" } }] + t.body.formData = [{ id: "f", key: "file", value: "", enabled: true, valueType: "file", fileName: "a.bin", fileContentBase64: "AAAA" }] + return t + } + + it("always drops runtime + refetchable state, keeps examples and file bytes", () => { + const [s] = slimTabsForStorage([fat()]) + expect(s.response).toBeUndefined() + expect(s.isLoading).toBeUndefined() + expect(s.websocket).toBeUndefined() + expect(s.graphqlSchema).toBeUndefined() + expect(s.examples[0].response.body).toBe("big") + expect(s.body.formData[0].fileContentBase64).toBe("AAAA") + }) + + it("aggressive mode also drops example bodies and file bytes but keeps metadata", () => { + const [s] = slimTabsForStorage([fat()], true) + expect(s.examples[0].name).toBe("ex") + expect(s.examples[0].response.body).toBe("") + expect(s.body.formData[0].fileName).toBe("a.bin") + expect(s.body.formData[0].fileContentBase64).toBe("") + }) +}) diff --git a/apps/desktop-ui/src/components/api-client/api-client.tsx b/apps/desktop-ui/src/components/api-client/api-client.tsx index 943e9519..acd566a5 100644 --- a/apps/desktop-ui/src/components/api-client/api-client.tsx +++ b/apps/desktop-ui/src/components/api-client/api-client.tsx @@ -1,6 +1,6 @@ "use client" -import { apiFetch } from "@/lib/desktop/api-fetch"; +import { apiFetch, sendProxyRequest } from "@/lib/desktop/api-fetch"; import * as React from "react" import { Card } from "@/components/ui/card" import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable" @@ -9,13 +9,8 @@ import { RequestTabs } from "./request-tabs" import { ResponsePanel } from "./response-panel" import { TabBar } from "./tab-bar" import { ImportCurlDialog, type ImportCurlTarget } from "./import-curl-dialog" -import { ImportDialog } from "./import-dialog" -import { CookieJarDialog } from "./cookie-jar-dialog" -import { WebSocketPanel } from "./websocket-panel" -import { GrpcPanel } from "./grpc-panel" import { SaveExampleDialog } from "./save-example-dialog" import { loadPlugins, instantiatePlugin, applyBeforeSend, applyAfterResponse, type PluginInstance } from "@/lib/plugins/plugin-runtime" -import { MetricsDialog } from "./metrics-dialog" import { recordMetric, recordLog } from "@/lib/observability/metrics" import { OfflineIndicator } from "./offline-indicator" import { putCachedResponse, getCachedResponse } from "@/lib/cache/response-cache" @@ -40,6 +35,7 @@ import { API_CLIENT_ERROR_STATUS_TEXT, ScriptTestResult, ScriptLog, + type ApiResponse, } from "./types" import type { ScriptContext } from "./workers/scripts-runner.worker" import { useTranslations } from "next-intl" @@ -78,6 +74,13 @@ const CodeGenerator = dynamic( () => import("./code-generator").then(m => ({ default: m.CodeGenerator })), { ssr: false, loading: () => null } ) +// Heavy or rarely-used surfaces stay out of the route's critical chunk: +// gRPC pulls protobufjs, the importer pulls js-yaml + every importer. +const GrpcPanel = dynamic(() => import("./grpc-panel").then(m => ({ default: m.GrpcPanel })), { ssr: false, loading: () => null }) +const WebSocketPanel = dynamic(() => import("./websocket-panel").then(m => ({ default: m.WebSocketPanel })), { ssr: false, loading: () => null }) +const ImportDialog = dynamic(() => import("./import-dialog").then(m => ({ default: m.ImportDialog })), { ssr: false, loading: () => null }) +const CookieJarDialog = dynamic(() => import("./cookie-jar-dialog").then(m => ({ default: m.CookieJarDialog })), { ssr: false, loading: () => null }) +const MetricsDialog = dynamic(() => import("./metrics-dialog").then(m => ({ default: m.MetricsDialog })), { ssr: false, loading: () => null }) /** * Tag history entries with the HTTP method when falling back to URL, so the list @@ -107,6 +110,7 @@ function buildRequestUrl(raw: string): URL { function ApiClientInner() { const t = useTranslations("ApiClient") + const tNav = useTranslations("Navigation") const { tabs, activeTabId, activeTab } = useTabs() const { addTab, appendTab, closeTab, duplicateTab, renameTab, reorderTabs, setActiveTabId, updateActiveTab } = useTabsActions() @@ -201,11 +205,11 @@ function ApiClientInner() { toast.success(`Saved example "${name}"`) } - const handleDeleteExample = (id: string) => { - updateActiveTab({ examples: (activeTab.examples ?? []).filter((e) => e.id !== id) }) - } + const handleDeleteExample = React.useCallback((id: string) => { + updateActiveTab((tab) => ({ examples: (tab.examples ?? []).filter((e) => e.id !== id) })) + }, [updateActiveTab]) - const handleLoadExample = (example: SavedExample) => { + const handleLoadExample = React.useCallback((example: SavedExample) => { updateActiveTab({ response: { status: example.response.status, @@ -219,7 +223,25 @@ function ApiClientInner() { isLoading: false, }) toast.success(`Loaded example "${example.name}"`) - } + }, [updateActiveTab]) + + // One stable setter per RequestTabs field. Inline `(v) => updateActiveTab({...})` + // props defeated RequestTabs' memo and re-rendered the whole editor per keystroke. + const setField = React.useMemo(() => { + const make = (key: K) => + (value: ApiRequestState[K]) => updateActiveTab({ [key]: value } as Partial) + return { + params: make("params"), + headers: make("headers"), + body: make("body"), + auth: make("auth"), + preRequestScript: make("preRequestScript"), + testScript: make("testScript"), + graphqlSchema: make("graphqlSchema"), + comments: make("comments"), + } + }, [updateActiveTab]) + const openSaveExample = React.useCallback(() => setSaveExampleOpen(true), []) // Scroll position memory for mobile panel toggle const scrollMemory = React.useRef<{ request: number; response: number }>({ request: 0, response: 0 }) @@ -302,12 +324,16 @@ function ApiClientInner() { } const { copyToClipboard } = useCopyToClipboard() + // Latest-value ref so callbacks handed to memoized children (RequestPanel) + // stay referentially stable across keystrokes. + const activeTabRef = React.useRef(activeTab) + React.useEffect(() => { activeTabRef.current = activeTab }, [activeTab]) const handleCopyCurl = React.useCallback(() => { - void copyToClipboard(generateCode(activeTab, "curl"), { + void copyToClipboard(generateCode(activeTabRef.current, "curl"), { successMessage: t("toasts.curlCopied"), errorMessage: t("toasts.copyFailed"), }) - }, [activeTab, copyToClipboard, t]) + }, [copyToClipboard, t]) const handleSaveRequest = (parentId: string, name: string) => { const requestToSave: CollectionRequest = { @@ -324,7 +350,7 @@ function ApiClientInner() { updateActiveTab({ name }) } - const handleLoadRequest = (request: CollectionRequest) => { + const handleLoadRequest = React.useCallback((request: CollectionRequest) => { // Locate the request inside any collection so we can fold ancestor folders' // defaults (headers / auth / scripts) into the materialised tab. A request // edited in a tab is a snapshot — later folder-default edits don't propagate. @@ -344,15 +370,15 @@ function ApiClientInner() { isLoading: false, } appendTab(newTab) - } + }, [collections, appendTab]) - const handleCancel = () => { + const handleCancel = React.useCallback(() => { abortControllerRef.current?.abort() abortControllerRef.current = null updateActiveTab({ isLoading: false }) - } + }, [updateActiveTab]) - const handleSend = React.useCallback(async () => { + const sendImpl = React.useCallback(async () => { if (!activeTab.url?.trim()) return abortControllerRef.current?.abort() @@ -441,8 +467,19 @@ function ApiClientInner() { // Resolve cross-tool secrets referenced anywhere in the request // (scan the raw tab JSON so params/headers/auth/body are all covered). // Throws a user-facing message when the vault is locked → outer catch. + // Only the editable request parts — stringifying the whole tab dragged + // the previous response body and every saved example through the scan. secretVars = await resolveSecretVariables( - [workUrl, JSON.stringify(workHeaders), JSON.stringify(activeTab)], + [ + workUrl, + JSON.stringify(workHeaders), + JSON.stringify({ + params: activeTab.params, + headers: activeTab.headers, + auth: activeTab.auth, + body: { ...activeTab.body, formData: activeTab.body.formData?.map((f) => ({ ...f, fileContentBase64: undefined })) }, + }), + ], cipherKey, ) @@ -774,24 +811,23 @@ function ApiClientInner() { Object.assign(headersObj, beforeApplied.req.headers) const finalUrlFromPlugins = beforeApplied.req.url - // Send via Proxy - const res = await apiFetch("/api/proxy", { - method: "POST", - credentials: "include", - signal: controller.signal, - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - url: finalUrlFromPlugins, - method: beforeApplied.req.method, - headers: headersObj, - body: beforeApplied.req.body ?? bodyPayload ?? bodyContent, - timeoutMs: activeTab.timeoutMs, - }), - }) - - const proxyData = await res.json() + // Send via Proxy (envelope returned directly — no fake Response round-trip) + const rawEnvelope = await sendProxyRequest({ + url: finalUrlFromPlugins, + method: beforeApplied.req.method, + headers: headersObj, + body: beforeApplied.req.body ?? bodyPayload ?? bodyContent, + timeoutMs: activeTab.timeoutMs, + }, controller.signal) + const proxyData = { + ...rawEnvelope, + statusText: rawEnvelope.statusText ?? "", + headers: rawEnvelope.headers ?? {}, + body: rawEnvelope.body ?? "", + time: rawEnvelope.time ?? 0, + size: rawEnvelope.size ?? 0, + redirectChain: rawEnvelope.redirectChain as ApiResponse["redirectChain"], + } // Plugins: onAfterResponse hook gets the live response. applyAfterResponse(pluginInstancesRef.current, { @@ -862,10 +898,10 @@ function ApiClientInner() { }, response: { status: proxyData.status, - statusText: proxyData.statusText, - headers: proxyData.headers, - body: proxyData.body, - time: proxyData.time, + statusText: proxyData.statusText ?? "", + headers: proxyData.headers ?? {}, + body: proxyData.body ?? "", + time: proxyData.time ?? 0, }, environment: { ...activeEnvironmentVariables, ...scriptEnvOverlay }, variables: { ...sessionVarsRef.current }, @@ -911,15 +947,15 @@ function ApiClientInner() { updateActiveTab({ response: { status: proxyData.status, - statusText: proxyData.statusText, - headers: proxyData.headers, - body: formattedBody, + statusText: proxyData.statusText ?? "", + headers: proxyData.headers ?? {}, + body: formattedBody ?? "", isBase64: proxyData.isBase64, - time: proxyData.time, - size: proxyData.size, + time: proxyData.time ?? 0, + size: proxyData.size ?? 0, error: proxyData.error, setCookies: proxyData.setCookies, - redirectChain: proxyData.redirectChain, + redirectChain: proxyData.redirectChain as ApiResponse["redirectChain"], }, scriptResults: (scriptTests.length || scriptLogs.length || scriptErrors.length) ? { tests: scriptTests, logs: scriptLogs, errors: scriptErrors } @@ -1008,8 +1044,13 @@ function ApiClientInner() { cipherKey, t, ]) + // `sendImpl` changes identity on every keystroke (it closes over activeTab); + // the stable wrapper keeps RequestPanel memoized and the keydown listener put. + const sendRef = React.useRef(sendImpl) + React.useEffect(() => { sendRef.current = sendImpl }, [sendImpl]) + const handleSend = React.useCallback(() => sendRef.current(), []) - const handleCurlPaste = (curl: string) => { + const handleCurlPaste = React.useCallback((curl: string) => { try { const parsed = parseCurlCommand(curl) // No URL extracted → don't claim success and don't wipe the current URL. @@ -1029,7 +1070,7 @@ function ApiClientInner() { console.error(error) toast.error(t("toasts.curlParseFailed")) } - } + }, [updateActiveTab, replaceUrlWithEnvBaseUrl, t]) // Keyboard shortcuts. // Cmd/Ctrl+T and Cmd/Ctrl+W are hard-reserved by the browser (`preventDefault` @@ -1053,7 +1094,6 @@ function ApiClientInner() { } window.addEventListener("keydown", handleKeyDown) return () => window.removeEventListener("keydown", handleKeyDown) - // eslint-disable-next-line react-hooks/exhaustive-deps }, [addTab, closeTab, handleSend, activeTabId]) // Desktop request column — reused whether it sits full-width (no response yet) @@ -1077,25 +1117,25 @@ function ApiClientInner() {
updateActiveTab({ params })} + setParams={setField.params} headers={activeTab.headers} - setHeaders={(headers) => updateActiveTab({ headers })} + setHeaders={setField.headers} body={activeTab.body} - setBody={(body) => updateActiveTab({ body })} + setBody={setField.body} auth={activeTab.auth} - setAuth={(auth) => updateActiveTab({ auth })} + setAuth={setField.auth} preRequestScript={activeTab.preRequestScript} - setPreRequestScript={(s) => updateActiveTab({ preRequestScript: s })} + setPreRequestScript={setField.preRequestScript} testScript={activeTab.testScript} - setTestScript={(s) => updateActiveTab({ testScript: s })} + setTestScript={setField.testScript} graphqlUrl={activeTab.url} graphqlSchema={activeTab.graphqlSchema} - setGraphqlSchema={(s) => updateActiveTab({ graphqlSchema: s })} + setGraphqlSchema={setField.graphqlSchema} examples={activeTab.examples} onDeleteExample={handleDeleteExample} onLoadExample={handleLoadExample} comments={activeTab.comments} - setComments={(next) => updateActiveTab({ comments: next })} + setComments={setField.comments} />
@@ -1110,7 +1150,10 @@ function ApiClientInner() { className="mobile-nav-offset" >
-
+
+ {/* The panel header carries "Collections"; without this the tool + itself was unnamed, and unnamed entirely once collapsed. */} +

{tNav("apiClient")}

{isMobile ? ( /* Mobile: secondary actions collapsed into dropdown */ @@ -1199,7 +1242,7 @@ function ApiClientInner() { > -
+
@@ -1406,29 +1451,28 @@ function ApiClientInner() { onScroll={(e) => { scrollMemory.current.response = (e.target as HTMLDivElement).scrollTop }} >
- setSaveExampleOpen(true) : undefined} /> +
- ) : !activeTab.response && !activeTab.isLoading ? ( - /* Desktop: no response yet — request panel gets the full width */ -
- {desktopRequestContent} -
) : ( - /* Desktop: side-by-side resizable panels once a response exists / is loading */ + /* Desktop: the request panel is always mounted in the same group; the + response panel joins the split once a response exists / is loading. + Swapping the whole layout on Send remounted every Monaco editor. */ - + {desktopRequestContent} - - - - -
- setSaveExampleOpen(true) : undefined} /> -
-
+ {(activeTab.response || activeTab.isLoading) && ( + <> + + +
+ +
+
+ + )}
)}
diff --git a/apps/desktop-ui/src/components/api-client/collections/collections-sidebar.tsx b/apps/desktop-ui/src/components/api-client/collections/collections-sidebar.tsx index d4164ce7..00d6ae6d 100644 --- a/apps/desktop-ui/src/components/api-client/collections/collections-sidebar.tsx +++ b/apps/desktop-ui/src/components/api-client/collections/collections-sidebar.tsx @@ -6,15 +6,17 @@ import { Checkbox } from "@/components/ui/checkbox" import { ScrollArea } from "@/components/ui/scroll-area" import { Collection, CollectionRequest } from "../types" import { CollectionItem } from "./collection-item" -import { FolderPlus, Trash2, Pencil, MoreHorizontal, Search, X, FileDown, Play, Server, Link2, Globe } from "lucide-react" +import { FolderPlus, FolderGit2, Trash2, Pencil, MoreHorizontal, Search, X, FileDown, Play, Server, Link2, Globe } from "lucide-react" import { buildShareUrl } from "@/lib/share-link" import { apiFetch } from "@/lib/desktop/api-fetch" import { toast } from "sonner" -import { downloadCollectionAsPostman } from "@/lib/export/postman" -import { downloadCollectionAsOpenApi } from "@/lib/export/openapi" -import { downloadCollectionAsHar } from "@/lib/export/har" -import { downloadCollectionAsInsomnia } from "@/lib/export/insomnia" -import { CollectionRunnerDialog } from "../collection-runner-dialog" +import dynamic from "next/dynamic" +import type { HistoryRequest } from "../types" +// Runner (+ csv/junit libs) only loads when a run is opened. +const CollectionRunnerDialog = dynamic( + () => import("../collection-runner-dialog").then((m) => ({ default: m.CollectionRunnerDialog })), + { ssr: false, loading: () => null }, +) import { useWorkspacesContext } from "../context/workspaces-context" import { WorkspacesDialog } from "../workspaces-dialog" import { Briefcase } from "lucide-react" @@ -47,7 +49,7 @@ import { useHistoryState, useHistoryActions } from "../context/history-context" import { CollectionsSidebarSkeleton, HistoryListSkeleton } from "../skeletons" import { useDebouncedValue } from "@/lib/use-debounced-value" import { isDesktop } from "@/lib/desktop/is-desktop" -import { ToolSidebarActions, useToolSidebarPanel } from "@/components/tools/tool-sidebar" +import { ToolSidebarActions, useToolSidebarPanel, useToolSidebarRail } from "@/components/tools/tool-sidebar" interface CollectionsSidebarProps { onLoadRequest: (request: CollectionRequest) => void @@ -55,17 +57,22 @@ interface CollectionsSidebarProps { export function CollectionsSidebar({ onLoadRequest: loadRequest }: CollectionsSidebarProps) { const panel = useToolSidebarPanel() - // On mobile the panel is a sheet over the request pane — loading a request - // has to dismiss it or the loaded request stays hidden behind the sheet. + // While the panel floats (mobile sheet, or the desktop flyout from the + // collapsed rail), loading a request has to dismiss it or the loaded + // request stays hidden behind the panel. const onLoadRequest = React.useCallback( (request: CollectionRequest) => { loadRequest(request) - if (panel?.isMobile) panel.close() + if (panel?.isOverlay) panel.close() }, [loadRequest, panel], ) const { collections, isLoading } = useCollectionsState() - const { addFolder: onAddFolder, deleteItem: onDelete, toggleFolder: onToggle, createCollection: onCreateCollection, renameCollection: onRenameCollection, renameFolder: onRenameFolder, patchFolder, deleteMultipleCollections: onDeleteMultiple } = useCollectionsActions() + const { addFolder: onAddFolder, deleteItem: onDelete, toggleFolder: onToggle, createCollection: onCreateCollection, renameCollection: onRenameCollection, renameFolder: onRenameFolder, patchFolder, deleteMultipleCollections: onDeleteMultiple, openFolderCollection } = useCollectionsActions() + + // isDesktop() is false during SSR — gate behind a mounted flag to avoid hydration mismatch. + const [desktop, setDesktop] = React.useState(false) + React.useEffect(() => { setDesktop(isDesktop()) }, []) const { history, isLoading: isHistoryLoading } = useHistoryState() const { clearHistory: onClearHistory, deleteHistoryItem: onDeleteHistoryItem } = useHistoryActions() const t = useTranslations("ApiClient.collectionsSidebar") @@ -137,6 +144,31 @@ export function CollectionsSidebar({ onLoadRequest: loadRequest }: CollectionsSi return filteredCollections.filter((c) => c.workspace === activeWorkspaceId) }, [filteredCollections, activeWorkspaceId]) + // Collections stay reachable from the collapsed rail. A collection has no + // open/closed state of its own — only the folders inside it toggle — so + // picking one expands the panel and scrolls that collection into view. + useToolSidebarRail( + "collections", + React.useMemo( + () => + collectionsForActiveWs.slice(0, 8).map((c) => ({ + id: c.id, + label: c.name, + icon: FolderGit2, + count: c.items.length, + onSelect: () => { + // The panel is expanding in the same tick; wait for layout. + requestAnimationFrame(() => + document + .querySelector(`[data-collection-id="${CSS.escape(c.id)}"]`) + ?.scrollIntoView({ block: "nearest" }), + ) + }, + })), + [collectionsForActiveWs], + ), + ) + const activeWorkspaceName = workspaces.find((w) => w.id === activeWorkspaceId)?.name ?? "All" const [selectedCollections, setSelectedCollections] = React.useState>(new Set()) const [deleteBulkDialogOpen, setDeleteBulkDialogOpen] = React.useState(false) @@ -195,6 +227,65 @@ export function CollectionsSidebar({ onLoadRequest: loadRequest }: CollectionsSi setSelectedCollections(new Set()) } + // Stable row renderer so VirtualHistoryList's memo holds while the request + // editor re-renders on every keystroke. + const renderHistoryRow = React.useCallback((item: HistoryRequest, style: React.CSSProperties) => ( +
+
onLoadRequest(item)} + > +
+
+ + {item.method} + + + {item.name ? getApiClientRequestDisplayName(item.name, tRoot) : item.url} + +
+ +
+
+ {item.url} +
+ {new Date(item.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} + {item.status && ( + = 200 && item.status < 300 ? "text-green-500" + : item.status >= 300 && item.status < 400 ? "text-amber-500" + : "text-destructive" + )}> + {item.status} + + )} +
+
+
+
+ ), [onLoadRequest, onDeleteHistoryItem, tRoot]) + return ( // Header (icon, title, collapse) is owned by ToolSidebarLayout in // components/api-client/api-client.tsx — this renders the body only. @@ -220,6 +311,17 @@ export function CollectionsSidebar({ onLoadRequest: loadRequest }: CollectionsSi > + {desktop && ( + + )} @@ -273,13 +375,21 @@ export function CollectionsSidebar({ onLoadRequest: loadRequest }: CollectionsSi

{t("noCollectionsHint")}

- +
+ + {desktop && ( + + )} +
) : ( collectionsForActiveWs.map((collection) => ( -
+
@@ -292,6 +402,11 @@ export function CollectionsSidebar({ onLoadRequest: loadRequest }: CollectionsSi {collection.name} + {collection.source && ( + + + + )}
-
-
- {item.url} -
- {new Date(item.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} - {item.status && ( - = 200 && item.status < 300 ? "text-green-500" - : item.status >= 300 && item.status < 400 ? "text-amber-500" - : "text-destructive" - )}> - {item.status} - - )} -
-
-
-
- )} + renderRow={renderHistoryRow} /> )}
diff --git a/apps/desktop-ui/src/components/api-client/collections/use-collections.ts b/apps/desktop-ui/src/components/api-client/collections/use-collections.ts index 4a7d64ae..bf2edc67 100644 --- a/apps/desktop-ui/src/components/api-client/collections/use-collections.ts +++ b/apps/desktop-ui/src/components/api-client/collections/use-collections.ts @@ -6,6 +6,10 @@ import { toast } from "sonner" import useAuth from "@/utils/useAuth" import { apiFetch } from "@/lib/desktop/api-fetch" import { broadcastApiClientUpdate, useApiClientSyncListener } from "@/lib/api-client-sync" +import { isDesktop } from "@/lib/desktop/is-desktop" +import { countLiteralAuthSecrets, diffFiles, newManifest, parseCollection, serializeCollection, MANIFEST_FILE } from "@/lib/api-client/file-store" +import { moveCollectionSecretsToVault } from "@/lib/api-client/move-to-vault" +import { useCipherKey } from "@/lib/use-cipher-key" const STORAGE_KEY = "api-client-collections" @@ -13,11 +17,154 @@ function sortCollections(cols: Collection[]) { return [...cols].sort((a, b) => a.name.localeCompare(b.name)) } +/** Carry UI-only open/closed state (stripped from disk) across reloads, by folder id. */ +function applyOpenState(prev: Collection | undefined, next: Collection): Collection { + if (!prev) return next + const openIds = new Set() + const collect = (items: Collection["items"]) => { + for (const item of items) { + if ("type" in item && item.type === "folder") { + if (item.isOpen) openIds.add(item.id) + collect(item.items) + } + } + } + collect(prev.items) + const apply = (items: Collection["items"]): Collection["items"] => + items.map((item) => + "type" in item && item.type === "folder" + ? { ...item, isOpen: openIds.has(item.id), items: apply(item.items) } + : item + ) + return { ...next, items: apply(next.items) } +} + export function useCollections() { const { user, loading } = useAuth() const [collections, setCollections] = React.useState([]) + const [fileCollections, setFileCollections] = React.useState([]) const [isLoading, setIsLoading] = React.useState(true) const migrationRanRef = React.useRef(false) + /** Paths already toasted about, so a failing folder doesn't re-toast on every window focus. */ + const failedFilePathsRef = React.useRef(new Set()) + /** Collections already warned about literal credentials — once per session is enough. */ + const secretWarnedRef = React.useRef(new Set()) + const cipherKey = useCipherKey() + // Latest file collections for toast-action callbacks (they outlive the closing render). + const fileCollectionsRef = React.useRef(fileCollections) + fileCollectionsRef.current = fileCollections + + // DB collections + folder-backed collections, one list for consumers. + const allCollections = React.useMemo( + () => sortCollections([...collections, ...fileCollections]), + [collections, fileCollections] + ) + + /** (Re-)read every registered folder collection from disk. Desktop only. */ + const reloadFileCollections = React.useCallback(async () => { + if (!isDesktop()) return + const { readCollectionFiles, allowCollectionDir, loadCollectionRegistry } = await import( + "@/lib/desktop/collection-files" + ) + const paths = await loadCollectionRegistry() + if (paths.length === 0) { + setFileCollections((cur) => (cur.length === 0 ? cur : [])) + return + } + const loaded: Collection[] = [] + for (const path of paths) { + try { + await allowCollectionDir(path) + const { collection, warnings } = parseCollection(await readCollectionFiles(path)) + warnings.forEach((w) => console.warn(`[file-collection] ${path}: ${w}`)) + loaded.push({ ...collection, source: { kind: "file", path } }) + failedFilePathsRef.current.delete(path) + } catch (e) { + console.error(`Failed to read folder collection at ${path}`, e) + if (!failedFilePathsRef.current.has(path)) { + failedFilePathsRef.current.add(path) + toast.error(`Couldn't read folder collection: ${path}`) + } + } + } + setFileCollections((cur) => { + const next = loaded.map((col) => applyOpenState(cur.find((c) => c.id === col.id), col)) + return JSON.stringify(next) === JSON.stringify(cur) ? cur : next + }) + }, []) + + // Load folder collections on mount and when the window regains focus + // (catches git pulls / external editor changes — no file watcher in v1). + React.useEffect(() => { + if (!isDesktop()) return + void reloadFileCollections() + const onFocus = () => void reloadFileCollections() + window.addEventListener("focus", onFocus) + return () => window.removeEventListener("focus", onFocus) + }, [reloadFileCollections]) + + /** + * Persist a mutated folder collection: swap state optimistically, then write + * only the changed files (deterministic serializer ⇒ no-op edits write nothing). + * On write failure the previous state is restored — never silently drop edits. + */ + const mutateFileCollection = async ( + target: Collection, + nextItems: Collection["items"], + patch?: Partial + ): Promise => { + const next = { ...target, ...patch, items: nextItems } + setFileCollections((cur) => cur.map((c) => (c.id === target.id ? next : c))) + // Literal credentials are redacted by the serializer (never written to git-able + // files) — tell the user once so a "missing" token isn't a mystery, and offer + // to move them into the encrypted vault (rewrites fields to {{vault.*}} tokens). + if (!secretWarnedRef.current.has(target.id) && countLiteralAuthSecrets(next) > 0) { + secretWarnedRef.current.add(target.id) + toast.warning( + "Credentials typed directly are not saved into collection files — keep them in the encrypted vault as {{vault.NAME}} tokens", + { + duration: 10000, + action: { + label: "Move to vault", + onClick: () => void moveFileCollectionSecrets(target.id), + }, + } + ) + } + try { + const { writes, deletes } = diffFiles(serializeCollection(target), serializeCollection(next)) + if (writes.length > 0 || deletes.length > 0) { + const { writeCollectionFiles } = await import("@/lib/desktop/collection-files") + await writeCollectionFiles(target.source!.path, writes, deletes) + } + return true + } catch (e) { + setFileCollections((cur) => cur.map((c) => (c.id === target.id ? target : c))) + console.error("Error writing folder collection", e) + toast.error(`Failed to write collection files: ${e instanceof Error ? e.message : String(e)}`) + return false + } + } + + /** Toast action: move every literal credential in a folder collection into the vault. */ + const moveFileCollectionSecrets = async (collectionId: string) => { + const target = fileCollectionsRef.current.find((c) => c.id === collectionId) + if (!target) return + try { + const { collection: moved, moved: count } = await moveCollectionSecretsToVault(target, cipherKey) + if (count === 0) return + if (await mutateFileCollection(target, moved.items)) { + toast.success( + count === 1 + ? "1 credential moved to the vault" + : `${count} credentials moved to the vault` + ) + } + } catch (e) { + console.error("Error moving credentials to vault", e) + toast.error(e instanceof Error ? e.message : "Failed to move credentials to the vault") + } + } React.useEffect(() => { if (!user) migrationRanRef.current = false @@ -145,8 +292,6 @@ export function useCollections() { ) const addFolder = async (parentId: string, name: string) => { - if (!user) return - const newFolder: CollectionFolder = { id: crypto.randomUUID(), name, @@ -155,12 +300,22 @@ export function useCollections() { isOpen: true, } - const targetCollection = collections.find(c => + const targetCollection = allCollections.find(c => c.id === parentId || findItemInCollection(c.items, parentId) ) if (!targetCollection) return + if (targetCollection.source) { + const nextItems = targetCollection.id === parentId + ? [...targetCollection.items, newFolder] + : addItemToParent(targetCollection.items, parentId, newFolder) + const ok = await mutateFileCollection(targetCollection, nextItems) + return ok ? newFolder.id : undefined + } + + if (!user) return + // Optimistic update const prev = collections const optimisticItems = targetCollection.id === parentId @@ -187,13 +342,28 @@ export function useCollections() { } const deleteItem = async (itemId: string) => { - if (!user) return - // Find collection containing item - const targetCollection = collections.find(c => + const targetCollection = allCollections.find(c => c.items.some(i => i.id === itemId) || findItemInCollection(c.items, itemId) ) + if (targetCollection?.source) { + await mutateFileCollection(targetCollection, deleteFromItems(targetCollection.items, itemId)) + return + } + + // Deleting a folder collection = forget it, never touch the user's files. + const fileCol = fileCollections.find((c) => c.id === itemId) + if (fileCol?.source) { + const { loadCollectionRegistry, saveCollectionRegistry } = await import("@/lib/desktop/collection-files") + await saveCollectionRegistry((await loadCollectionRegistry()).filter((p) => p !== fileCol.source!.path)) + setFileCollections((cur) => cur.filter((c) => c.id !== itemId)) + toast.success("Folder collection removed from list (files kept)") + return + } + + if (!user) return + // If not found in items, maybe it IS the collection? if (!targetCollection) { const colToDelete = collections.find(c => c.id === itemId) @@ -236,14 +406,22 @@ export function useCollections() { } const saveRequest = async (parentId: string, request: CollectionRequest) => { - if (!user) return - - const targetCollection = collections.find(c => + const targetCollection = allCollections.find(c => c.id === parentId || findItemInCollection(c.items, parentId) ) if (!targetCollection) return + if (targetCollection.source) { + const nextItems = targetCollection.id === parentId + ? [...targetCollection.items, request] + : addItemToParent(targetCollection.items, parentId, request) + if (await mutateFileCollection(targetCollection, nextItems)) toast.success("Request saved") + return + } + + if (!user) return + // Optimistic update const prev = collections const optimisticItems = targetCollection.id === parentId @@ -270,11 +448,19 @@ export function useCollections() { } const toggleFolder = async (folderId: string) => { - if (!user) return - - const targetCollection = collections.find(c => findItemInCollection(c.items, folderId)) + const targetCollection = allCollections.find(c => findItemInCollection(c.items, folderId)) if (!targetCollection) return + if (targetCollection.source) { + // isOpen is UI-only and stripped from disk — pure state change. + setFileCollections((cur) => + cur.map((c) => (c.id === targetCollection.id ? { ...c, items: toggleInItems(c.items, folderId) } : c)) + ) + return + } + + if (!user) return + // Optimistic update (toggle is purely local UX — we still persist to avoid drift) const prev = collections const optimisticItems = toggleInItems(targetCollection.items, folderId) @@ -306,10 +492,8 @@ export function useCollections() { * Applies optimistically against state, reconciles from the delta result. */ const patchFolder = async (folderId: string, patch: Partial) => { - if (!user) return - const targetCollection = collections.find((c) => findItemInCollection(c.items, folderId)) + const targetCollection = allCollections.find((c) => findItemInCollection(c.items, folderId)) if (!targetCollection) return - const prev = collections const patchInItems = (items: (CollectionFolder | CollectionRequest)[]): (CollectionFolder | CollectionRequest)[] => items.map((item) => { if ("type" in item && item.type === "folder") { @@ -318,6 +502,16 @@ export function useCollections() { } return item }) + + if (targetCollection.source) { + if (await mutateFileCollection(targetCollection, patchInItems(targetCollection.items))) { + toast.success("Folder defaults saved") + } + return + } + + if (!user) return + const prev = collections setCollections((cur) => sortCollections(cur.map((c) => c.id === targetCollection.id ? { ...c, items: patchInItems(c.items) } : c @@ -338,11 +532,16 @@ export function useCollections() { } const renameFolder = async (folderId: string, name: string) => { - if (!user) return - - const targetCollection = collections.find(c => findItemInCollection(c.items, folderId)) + const targetCollection = allCollections.find(c => findItemInCollection(c.items, folderId)) if (!targetCollection) return + if (targetCollection.source) { + await mutateFileCollection(targetCollection, renameFolderInItems(targetCollection.items, folderId, name)) + return + } + + if (!user) return + // Optimistic update const prev = collections const optimisticItems = renameFolderInItems(targetCollection.items, folderId, name) @@ -498,6 +697,14 @@ export function useCollections() { } const renameCollection = async (collectionId: string, name: string) => { + const fileCol = fileCollections.find((c) => c.id === collectionId) + if (fileCol) { + if (await mutateFileCollection(fileCol, fileCol.items, { name })) { + toast.success("Collection renamed") + } + return + } + if (!user) return const prev = collections // Optimistic update @@ -521,9 +728,26 @@ export function useCollections() { } const deleteMultipleCollections = async (ids: string[]) => { - if (!user) return if (ids.length === 0) return + // Folder collections: forget only, never delete the user's files. + const fileIds = new Set(fileCollections.filter((c) => ids.includes(c.id)).map((c) => c.id)) + if (fileIds.size > 0) { + const removedPaths = new Set( + fileCollections.filter((c) => fileIds.has(c.id)).map((c) => c.source!.path) + ) + const { loadCollectionRegistry, saveCollectionRegistry } = await import("@/lib/desktop/collection-files") + await saveCollectionRegistry((await loadCollectionRegistry()).filter((p) => !removedPaths.has(p))) + setFileCollections((cur) => cur.filter((c) => !fileIds.has(c.id))) + ids = ids.filter((id) => !fileIds.has(id)) + if (ids.length === 0) { + toast.success("Folder collections removed from list (files kept)") + return + } + } + + if (!user) return + try { // Use Promise.allSettled to handle partial failures gracefully const deleteResults = await Promise.allSettled( @@ -581,8 +805,52 @@ export function useCollections() { } } + /** + * Pick a folder and open it as a file-backed collection. A folder with a + * `collection.yaml` is opened as-is; a folder without one is initialized + * (name = folder basename). Desktop only. + */ + const openFolderCollection = async (): Promise => { + if (!isDesktop()) return null + try { + const { pickFolder, readCollectionFiles, writeCollectionFiles, loadCollectionRegistry, saveCollectionRegistry } = + await import("@/lib/desktop/collection-files") + const path = await pickFolder() + if (!path) return null + + const registry = await loadCollectionRegistry() + if (registry.includes(path)) { + toast.info("This folder collection is already open") + return fileCollections.find((c) => c.source?.path === path) ?? null + } + + let col: Collection + const entries = await readCollectionFiles(path) + if (entries.some((e) => e.relPath === MANIFEST_FILE)) { + const { collection, warnings } = parseCollection(entries) + warnings.forEach((w) => console.warn(`[file-collection] ${path}: ${w}`)) + col = { ...collection, source: { kind: "file", path } } + } else { + const name = path.split(/[/\\]/).filter(Boolean).pop() || "Collection" + const manifest = newManifest(name) + await writeCollectionFiles(path, [{ relPath: MANIFEST_FILE, content: manifest.content }], []) + col = { id: manifest.id, name, items: [], source: { kind: "file", path } } + } + + await saveCollectionRegistry([...registry, path]) + setFileCollections((cur) => [...cur.filter((c) => c.id !== col.id), col]) + toast.success(`Opened folder collection "${col.name}"`) + return col + } catch (e) { + console.error("Error opening folder collection", e) + toast.error(`Couldn't open folder as collection: ${e instanceof Error ? e.message : String(e)}`) + return null + } + } + return { - collections, + collections: allCollections, + openFolderCollection, addFolder, deleteItem, saveRequest, diff --git a/apps/desktop-ui/src/components/api-client/context/collections-context.tsx b/apps/desktop-ui/src/components/api-client/context/collections-context.tsx index 5003f54b..1522a203 100644 --- a/apps/desktop-ui/src/components/api-client/context/collections-context.tsx +++ b/apps/desktop-ui/src/components/api-client/context/collections-context.tsx @@ -22,6 +22,8 @@ type CollectionsActions = { patchFolder: (folderId: string, patch: Partial) => Promise deleteMultipleCollections: (ids: string[]) => Promise importCollection: (incoming: Collection) => Promise + /** Desktop only: open (or initialize) a folder of YAML files as a git-friendly collection. */ + openFolderCollection: () => Promise } // ── Contexts ─────────────────────────────────────────────────────────────── @@ -45,6 +47,7 @@ export function CollectionsProvider({ children }: { children: React.ReactNode }) patchFolder, deleteMultipleCollections, importCollection, + openFolderCollection, } = useCollections() const state = React.useMemo( @@ -69,9 +72,10 @@ export function CollectionsProvider({ children }: { children: React.ReactNode }) patchFolder, deleteMultipleCollections, importCollection, + openFolderCollection, }), // eslint-disable-next-line react-hooks/exhaustive-deps - [addFolder, deleteItem, saveRequest, toggleFolder, createCollection, renameCollection, renameFolder, patchFolder, deleteMultipleCollections, importCollection] + [addFolder, deleteItem, saveRequest, toggleFolder, createCollection, renameCollection, renameFolder, patchFolder, deleteMultipleCollections, importCollection, openFolderCollection] ) return ( diff --git a/apps/desktop-ui/src/components/api-client/context/tabs-context.tsx b/apps/desktop-ui/src/components/api-client/context/tabs-context.tsx index e1ecbbc9..4ccba53a 100644 --- a/apps/desktop-ui/src/components/api-client/context/tabs-context.tsx +++ b/apps/desktop-ui/src/components/api-client/context/tabs-context.tsx @@ -6,6 +6,40 @@ import { ApiRequestState, API_CLIENT_DEFAULT_TAB_NAME } from "../types" const TABS_STORAGE_KEY = "api-client-tabs" const ACTIVE_TAB_STORAGE_KEY = "api-client-active-tab" +/** + * Shape a tab for localStorage. Always drops runtime state (response, loading, + * websocket transcript) and the re-fetchable GraphQL schema. With `aggressive` + * it also drops saved-example bodies and uploaded file bytes — used only as a + * fallback when the full copy overflows the ~5 MB quota, so one big example + * can't wipe every open tab. + */ +export function slimTabsForStorage(tabs: ApiRequestState[], aggressive = false): Partial[] { + return tabs.map((t) => { + const { response: _r, isLoading: _l, websocket: _ws, graphqlSchema: _gs, ...rest } = t + if (!aggressive) return rest + return { + ...rest, + examples: rest.examples?.map((e) => ({ ...e, response: { ...e.response, body: "" } })), + body: { + ...rest.body, + formData: rest.body.formData?.map((f) => + f.fileContentBase64 ? { ...f, fileContentBase64: "" } : f), + }, + } + }) +} + +function persistTabs(tabs: ApiRequestState[]) { + for (const aggressive of [false, true]) { + try { + localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(slimTabsForStorage(tabs, aggressive))) + return + } catch (e) { + console.warn(`api-client tabs: localStorage write failed (aggressive=${aggressive})`, e) + } + } +} + export const createNewTab = (kind: "rest" | "websocket" | "grpc" = "rest"): ApiRequestState => ({ id: crypto.randomUUID(), name: kind === "websocket" ? "New WebSocket" : kind === "grpc" ? "New gRPC" : API_CLIENT_DEFAULT_TAB_NAME, @@ -101,42 +135,18 @@ export function TabsProvider({ children }: { children: React.ReactNode }) { activeTabIdRef.current = activeTabId }) - // Save tabs to localStorage — strip `response`/`isLoading` (responses can be MBs and - // would blow the per-origin localStorage quota). Writes are debounced to 500ms so rapid - // edits (e.g. typing in the URL input) don't saturate the storage layer. + // Save tabs to localStorage. Writes are debounced to 500ms so rapid edits + // (e.g. typing in the URL input) don't saturate the storage layer. React.useEffect(() => { if (!isInitialized) return - const timeoutId = setTimeout(() => { - const slim = tabsRef.current.map((t) => { - // Drop response (big) + isLoading + websocket runtime (transcript can be huge) - // before persisting. WS config (status/draft/protocols) is rebuilt fresh on reload. - const { response: _r, isLoading: _l, websocket: _ws, ...rest } = t - return rest - }) - try { - localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(slim)) - } catch (e) { - console.warn("api-client tabs: localStorage write failed, dropping persisted state", e) - try { localStorage.removeItem(TABS_STORAGE_KEY) } catch { /* noop */ } - } - }, 500) + const timeoutId = setTimeout(() => persistTabs(tabsRef.current), 500) return () => clearTimeout(timeoutId) }, [tabs, isInitialized]) // Flush the latest tab state immediately on unmount so mid-debounce edits // are not lost when the user navigates away. React.useEffect(() => { - return () => { - const slim = tabsRef.current.map((t) => { - // Drop response (big) + isLoading + websocket runtime (transcript can be huge) - // before persisting. WS config (status/draft/protocols) is rebuilt fresh on reload. - const { response: _r, isLoading: _l, websocket: _ws, ...rest } = t - return rest - }) - try { - localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(slim)) - } catch { /* noop */ } - } + return () => { persistTabs(tabsRef.current) } }, []) // Save active tab id to localStorage diff --git a/apps/desktop-ui/src/components/api-client/environment-manager.tsx b/apps/desktop-ui/src/components/api-client/environment-manager.tsx index c85b15b6..448b35c5 100644 --- a/apps/desktop-ui/src/components/api-client/environment-manager.tsx +++ b/apps/desktop-ui/src/components/api-client/environment-manager.tsx @@ -10,8 +10,9 @@ import { DialogTitle, } from "@/components/ui/dialog" import { ScrollArea } from "@/components/ui/scroll-area" -import { IconPlus, IconTrash, IconEdit } from "@tabler/icons-react" +import { IconPlus, IconTrash, IconEdit, IconDownload } from "@tabler/icons-react" import { EnvironmentVariable } from "./use-environments" +import { downloadEnvironmentAsPostman } from "@/lib/export/postman-env" import { useTranslations } from "next-intl" import { useEnvironmentsState, useEnvironmentsActions } from "./context/environments-context" @@ -140,6 +141,18 @@ export function EnvironmentManager({ open, onOpenChange }: EnvironmentManagerPro > + - {open && entries.map(([k, v]) => ( + {open && entries.slice(0, MAX_TREE_CHILDREN).map(([k, v]) => ( ))} + {open && entries.length > MAX_TREE_CHILDREN && ( + + )}
) } +// Nothing is virtualised here; a 10k-element array would otherwise mount ~100k +// DOM nodes when the Tree tab opens. Caps keep the views instant on big bodies. +const MAX_TREE_CHILDREN = 200 +const MAX_TABLE_ROWS = 500 + function Leaf({ name, text, tone, depth }: { name: string | number; text: string; tone: string; depth: number }) { return (
@@ -95,7 +103,7 @@ export function JsonTableView({ rows }: { rows: unknown }) { - {rows.map((r, i) => ( + {rows.slice(0, MAX_TABLE_ROWS).map((r, i) => ( {cols.map((c) => { const v = (r && typeof r === "object" ? (r as Record)[c] : undefined) @@ -109,6 +117,11 @@ export function JsonTableView({ rows }: { rows: unknown }) { ))} + {rows.length > MAX_TABLE_ROWS && ( +
+ Showing {MAX_TABLE_ROWS} of {rows.length} rows +
+ )}
) } diff --git a/apps/desktop-ui/src/components/api-client/key-value-editor.tsx b/apps/desktop-ui/src/components/api-client/key-value-editor.tsx index 89f48bb5..df0f0ab6 100644 --- a/apps/desktop-ui/src/components/api-client/key-value-editor.tsx +++ b/apps/desktop-ui/src/components/api-client/key-value-editor.tsx @@ -7,6 +7,10 @@ import { Input } from "@/components/ui/input" import { Checkbox } from "@/components/ui/checkbox" import { KeyValueItem } from "./types" import { useTranslations } from "next-intl" +import { cn } from "@/lib/utils" + +/** 2rem checkbox · key · value · 2.25rem delete button (h-9 w-9). */ +const ROW_GRID = "grid grid-cols-[2rem_minmax(0,1fr)_minmax(0,1fr)_2.25rem] items-center gap-2" interface KeyValueEditorProps { items: KeyValueItem[] @@ -36,15 +40,18 @@ export function KeyValueEditor({ items, onChange }: KeyValueEditorProps) { return (
-
-
-
{t("columnKey")}
-
{t("columnValue")}
-
+ {/* Header and rows share one grid template — a flex header with no + `gap` drifted out of line with the rows, which have one. */} +
+
+ {/* px-3 matches the Input's own padding so the label sits over its text */} +
{t("columnKey")}
+
{t("columnValue")}
+
{items.map((item) => ( -
-
+
+
@@ -56,19 +63,17 @@ export function KeyValueEditor({ items, onChange }: KeyValueEditorProps) { placeholder={t("placeholderKey")} value={item.key} onChange={(e) => updateItem(item.id, "key", e.target.value)} - className="flex-1" /> updateItem(item.id, "value", e.target.value)} - className="flex-1" /> diff --git a/apps/desktop-ui/src/components/api-client/offline-indicator.tsx b/apps/desktop-ui/src/components/api-client/offline-indicator.tsx index 94a23c2b..63c22249 100644 --- a/apps/desktop-ui/src/components/api-client/offline-indicator.tsx +++ b/apps/desktop-ui/src/components/api-client/offline-indicator.tsx @@ -44,21 +44,25 @@ export function OfflineIndicator() { // Hide when online + nothing queued. if (online && queue.length === 0) return null + // Carries its own separator so the toolbar has no dangling divider while hidden. return ( - + <> +
+ + ) } diff --git a/apps/desktop-ui/src/components/api-client/request-tabs.tsx b/apps/desktop-ui/src/components/api-client/request-tabs.tsx index d12e2e4f..b1ece110 100644 --- a/apps/desktop-ui/src/components/api-client/request-tabs.tsx +++ b/apps/desktop-ui/src/components/api-client/request-tabs.tsx @@ -69,7 +69,7 @@ interface RequestTabsProps { setComments?: (next: import("./types").RequestComment[]) => void } -export function RequestTabs({ +function RequestTabsImpl({ params, setParams, headers, @@ -105,9 +105,9 @@ export function RequestTabs({ ], }), [body]) - const updateBody = (updates: Partial) => { + const updateBody = React.useCallback((updates: Partial) => { setBody({ ...normalizedBody, ...updates }) - } + }, [normalizedBody, setBody]) const addFormDataItem = () => { updateBody({ @@ -157,10 +157,10 @@ export function RequestTabs({ const handleBodyContentChange = React.useCallback( (v: string) => updateBody({ content: v }), - // updateBody closes over normalizedBody via its own capture; listing - // updateBody as the dep is sufficient and avoids re-creating the - // callback on every normalizedBody identity change. - // eslint-disable-next-line react-hooks/exhaustive-deps + [updateBody] + ) + const handleGraphqlVariablesChange = React.useCallback( + (v: string) => updateBody({ graphqlVariables: v }), [updateBody] ) @@ -413,7 +413,9 @@ export function RequestTabs({ - + {/* Editor tabs stay mounted (forceMount) so switching sub-tabs doesn't + re-create a Monaco instance each time; `hidden` via data-state. */} +
+ + + + + {t('autoDetect')} + {LANGUAGES.map((l) => ( + + {l.label} + + ))} + + +
- -
- - -
+
+ + +
-
- - -
+
+ + setPadding(v[0] ?? 48)} + className="py-2" + /> +
-
- - setPadding(v[0] ?? 48)} - className="py-2" - /> -
+
+ + setFilename(e.target.value)} + placeholder={t('filenamePlaceholder')} + className="w-[170px]" + /> +
-
- - setFilename(e.target.value)} - placeholder={t('filenamePlaceholder')} - className="w-[170px]" - /> +
+ +
+
+
-
- -
- -
-
+
+ + v && setWindowTheme(v as 'dark' | 'light')} + className="justify-start" + > + + {t('themeDark')} + + + {t('themeLight')} + + +
-
- - v && setWindowTheme(v as 'dark' | 'light')} - className="justify-start" - > - - {t('themeDark')} +
+ + v && setScale(Number(v))} + className="justify-start" + > + {[1, 2, 3].map((s) => ( + + {s}x - - {t('themeLight')} - - -
+ ))} +
+
-
- - v && setScale(Number(v))} - className="justify-start" - > - {[1, 2, 3].map((s) => ( - - {s}x - - ))} - -
+
+ + +
+
+ ) -
- - -
-
+ return ( + + {/* Static stylesheet constant in this file. */} + {/* threatcrush-disable-next-line js-unescaped-html-sink */} +