diff --git a/.travis.yml b/.travis.yml index 9920f0ee1..41a1490f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,6 +44,6 @@ before_install: script: - | cd ElectronClient/app - rsync -aP ../../ReactNativeClient/lib/ lib/ + rsync -aP --delete ../../ReactNativeClient/lib/ lib/ npm install yarn dist diff --git a/BUILD.md b/BUILD.md index db6850666..63c1d71e5 100644 --- a/BUILD.md +++ b/BUILD.md @@ -21,7 +21,7 @@ If you get a node-gyp related error you might need to manually install it: `npm ``` cd ElectronClient/app -rsync -a ../../ReactNativeClient/lib/ lib/ +rsync --delete -a ../../ReactNativeClient/lib/ lib/ npm install yarn dist ``` @@ -44,7 +44,7 @@ Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios` cd CliClient npm install ./build.sh -rsync -aP ../ReactNativeClient/locales/ build/locales/ +rsync --delete -aP ../ReactNativeClient/locales/ build/locales/ ``` Run `run.sh` to start the application for testing. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..279be002a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# Adding new features +If you want to add a new feature, consider asking about it before implementing it to make sure it is within the scope of the project. Of course you are free to create the pull request directly but it is not guaranteed it is going to be accepted. + +# Style +- Only use tabs for indentation, not spaces. +- Do not remove or add optional characters from other lines (such as colons or new line characters) as it can make the commit needlessly big, and create conflicts with other changes. diff --git a/CliClient/app/ResourceServer.js b/CliClient/app/ResourceServer.js index cdeecc617..c7425d039 100644 --- a/CliClient/app/ResourceServer.js +++ b/CliClient/app/ResourceServer.js @@ -1,6 +1,6 @@ const { _ } = require('lib/locale.js'); const { Logger } = require('lib/logger.js'); -const { Resource } = require('lib/models/resource.js'); +const Resource = require('lib/models/Resource.js'); const { netUtils } = require('lib/net-utils.js'); const http = require("http"); diff --git a/CliClient/app/app-gui.js b/CliClient/app/app-gui.js index 39ec44a8b..906efeb06 100644 --- a/CliClient/app/app-gui.js +++ b/CliClient/app/app-gui.js @@ -1,9 +1,9 @@ const { Logger } = require('lib/logger.js'); -const { Folder } = require('lib/models/folder.js'); -const { Tag } = require('lib/models/tag.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Note } = require('lib/models/note.js'); -const { Resource } = require('lib/models/resource.js'); +const Folder = require('lib/models/Folder.js'); +const Tag = require('lib/models/Tag.js'); +const BaseModel = require('lib/BaseModel.js'); +const Note = require('lib/models/Note.js'); +const Resource = require('lib/models/Resource.js'); const { cliUtils } = require('./cli-utils.js'); const { reducer, defaultState } = require('lib/reducer.js'); const { splitCommandString } = require('lib/string-utils.js'); @@ -14,6 +14,7 @@ const chalk = require('chalk'); const tk = require('terminal-kit'); const TermWrapper = require('tkwidgets/framework/TermWrapper.js'); const Renderer = require('tkwidgets/framework/Renderer.js'); +const DecryptionWorker = require('lib/services/DecryptionWorker'); const BaseWidget = require('tkwidgets/BaseWidget.js'); const ListWidget = require('tkwidgets/ListWidget.js'); @@ -65,6 +66,7 @@ class AppGui { // a regular command it's not necessary since the process // exits right away. reg.setupRecurrentSync(); + DecryptionWorker.instance().scheduleStart(); } store() { @@ -80,8 +82,16 @@ class AppGui { await this.renderer_.renderRoot(); } - prompt(initialText = '', promptString = ':') { - return this.widget('statusBar').prompt(initialText, promptString); + termSaveState() { + return this.term().saveState(); + } + + termRestoreState(state) { + return this.term().restoreState(state); + } + + prompt(initialText = '', promptString = ':', options = null) { + return this.widget('statusBar').prompt(initialText, promptString, options); } stdoutMaxWidth() { @@ -548,6 +558,10 @@ class AppGui { } this.widget('console').scrollBottom(); + + // Invalidate so that the screen is redrawn in case inputting a command has moved + // the GUI up (in particular due to autocompletion), it's moved back to the right position. + this.widget('root').invalidate(); } async updateFolderList() { @@ -826,4 +840,4 @@ class AppGui { AppGui.INPUT_MODE_NORMAL = 1; AppGui.INPUT_MODE_META = 2; -module.exports = AppGui; \ No newline at end of file +module.exports = AppGui; diff --git a/CliClient/app/app.js b/CliClient/app/app.js index 04db4c663..00e50aed1 100644 --- a/CliClient/app/app.js +++ b/CliClient/app/app.js @@ -5,12 +5,12 @@ const { JoplinDatabase } = require('lib/joplin-database.js'); const { Database } = require('lib/database.js'); const { FoldersScreenUtils } = require('lib/folders-screen-utils.js'); const { DatabaseDriverNode } = require('lib/database-driver-node.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); -const { BaseItem } = require('lib/models/base-item.js'); -const { Note } = require('lib/models/note.js'); -const { Tag } = require('lib/models/tag.js'); -const { Setting } = require('lib/models/setting.js'); +const BaseModel = require('lib/BaseModel.js'); +const Folder = require('lib/models/Folder.js'); +const BaseItem = require('lib/models/BaseItem.js'); +const Note = require('lib/models/Note.js'); +const Tag = require('lib/models/Tag.js'); +const Setting = require('lib/models/Setting.js'); const { Logger } = require('lib/logger.js'); const { sprintf } = require('sprintf-js'); const { reg } = require('lib/registry.js'); @@ -144,13 +144,15 @@ class Application extends BaseApplication { message += ' (' + options.answers.join('/') + ')'; } - let answer = await this.gui().prompt('', message + ' '); + let answer = await this.gui().prompt('', message + ' ', options); if (options.type === 'boolean') { if (answer === null) return false; // Pressed ESCAPE if (!answer) answer = options.answers[0]; let positiveIndex = options.booleanAnswerDefault == 'y' ? 0 : 1; return answer.toLowerCase() === options.answers[positiveIndex].toLowerCase(); + } else { + return answer; } }); @@ -275,7 +277,7 @@ class Application extends BaseApplication { dummyGui() { return { isDummy: () => { return true; }, - prompt: (initialText = '', promptString = '') => { return cliUtils.prompt(initialText, promptString); }, + prompt: (initialText = '', promptString = '', options = null) => { return cliUtils.prompt(initialText, promptString, options); }, showConsole: () => {}, maximizeConsole: () => {}, stdout: (text) => { console.info(text); }, @@ -283,7 +285,10 @@ class Application extends BaseApplication { exit: () => {}, showModalOverlay: (text) => {}, hideModalOverlay: () => {}, - stdoutMaxWidth: () => { return 78; } + stdoutMaxWidth: () => { return 78; }, + forceRender: () => {}, + termSaveState: () => {}, + termRestoreState: (state) => {}, }; } @@ -351,7 +356,7 @@ class Application extends BaseApplication { this.dispatch({ type: 'TAG_UPDATE_ALL', - tags: tags, + items: tags, }); this.store().dispatch({ diff --git a/CliClient/app/autocompletion.js b/CliClient/app/autocompletion.js new file mode 100644 index 000000000..cca40e206 --- /dev/null +++ b/CliClient/app/autocompletion.js @@ -0,0 +1,185 @@ +var { app } = require('./app.js'); +var Note = require('lib/models/Note.js'); +var Folder = require('lib/models/Folder.js'); +var Tag = require('lib/models/Tag.js'); +var { cliUtils } = require('./cli-utils.js'); +var yargParser = require('yargs-parser'); + +async function handleAutocompletionPromise(line) { + // Auto-complete the command name + const names = await app().commandNames(); + let words = getArguments(line); + //If there is only one word and it is not already a command name then you + //should look for commmands it could be + if (words.length == 1) { + if (names.indexOf(words[0]) === -1) { + let x = names.filter((n) => n.indexOf(words[0]) === 0); + if (x.length === 1) { + return x[0] + ' '; + } + return x.length > 0 ? x.map((a) => a + ' ') : line; + } else { + return line; + } + } + //There is more than one word and it is a command + const metadata = (await app().commandMetadata())[words[0]]; + //If for some reason this command does not have any associated metadata + //just don't autocomplete. However, this should not happen. + if (metadata === undefined) { + return line; + } + //complete an option + let next = words.length > 1 ? words[words.length - 1] : ''; + let l = []; + if (next[0] === '-') { + for (let i = 0; i 1 && options[1].indexOf(next) === 0) { + l.push(options[1]); + } else if (options[0].indexOf(next) === 0) { + l.push(options[2]); + } + } + if (l.length === 0) { + return line; + } + let ret = l.map(a=>toCommandLine(a)); + ret.prefix = toCommandLine(words.slice(0, -1)) + ' '; + return ret; + } + //Complete an argument + //Determine the number of positional arguments by counting the number of + //words that don't start with a - less one for the command name + const positionalArgs = words.filter((a)=>a.indexOf('-') !== 0).length - 1; + + let cmdUsage = yargParser(metadata.usage)['_']; + cmdUsage.splice(0, 1); + + if (cmdUsage.length >= positionalArgs) { + + let argName = cmdUsage[positionalArgs - 1]; + argName = cliUtils.parseCommandArg(argName).name; + + if (argName == 'note' || argName == 'note-pattern' && app().currentFolder()) { + const notes = await Note.previews(app().currentFolder().id, { titlePattern: next + '*' }); + l.push(...notes.map((n) => n.title)); + } + + if (argName == 'notebook') { + const folders = await Folder.search({ titlePattern: next + '*' }); + l.push(...folders.map((n) => n.title)); + } + + if (argName == 'tag') { + let tags = await Tag.search({ titlePattern: next + '*' }); + l.push(...tags.map((n) => n.title)); + } + + if (argName == 'tag-command') { + let c = filterList(['add', 'remove', 'list'], next); + l.push(...c); + } + + if (argName == 'todo-command') { + let c = filterList(['toggle', 'clear'], next); + l.push(...c); + } + } + if (l.length === 1) { + return toCommandLine([...words.slice(0, -1), l[0]]); + } else if (l.length > 1) { + let ret = l.map(a=>toCommandLine(a)); + ret.prefix = toCommandLine(words.slice(0, -1)) + ' '; + return ret; + } + return line; + +} +function handleAutocompletion(str, callback) { + handleAutocompletionPromise(str).then(function(res) { + callback(undefined, res); + }); +} +function toCommandLine(args) { + if (Array.isArray(args)) { + return args.map(function(a) { + if(a.indexOf('"') !== -1 || a.indexOf(' ') !== -1) { + return "'" + a + "'"; + } else if (a.indexOf("'") !== -1) { + return '"' + a + '"'; + } else { + return a; + } + }).join(' '); + } else { + if(args.indexOf('"') !== -1 || args.indexOf(' ') !== -1) { + return "'" + args + "' "; + } else if (args.indexOf("'") !== -1) { + return '"' + args + '" '; + } else { + return args + ' '; + } + } +} +function getArguments(line) { + let inSingleQuotes = false; + let inDoubleQuotes = false; + let currentWord = ''; + let parsed = []; + for(let i = 0; i { - rl.question(message + ' ', (answer) => { - rl.close(); - resolve(answer); - }); - }); -} - // Note: initialText is there to have the same signature as statusBar.prompt() so that // it can be a drop-in replacement, however initialText is not used (and cannot be // with readline.question?). -cliUtils.prompt = function(initialText = '', promptString = ':') { +cliUtils.prompt = function(initialText = '', promptString = ':', options = null) { + if (!options) options = {}; + const readline = require('readline'); + const Writable = require('stream').Writable; + + const mutableStdout = new Writable({ + write: function(chunk, encoding, callback) { + if (!this.muted) + process.stdout.write(chunk, encoding); + callback(); + } + }); const rl = readline.createInterface({ input: process.stdin, - output: process.stdout + output: mutableStdout, + terminal: true, }); return new Promise((resolve, reject) => { + mutableStdout.muted = false; + rl.question(promptString, (answer) => { rl.close(); + if (!!options.secure) this.stdout_(''); resolve(answer); }); + + mutableStdout.muted = !!options.secure; }); } diff --git a/CliClient/app/command-attach.js b/CliClient/app/command-attach.js index ab7431710..0b22ef4cd 100644 --- a/CliClient/app/command-attach.js +++ b/CliClient/app/command-attach.js @@ -1,7 +1,7 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { BaseModel } = require('lib/base-model.js'); +const BaseModel = require('lib/BaseModel.js'); const { shim } = require('lib/shim.js'); const fs = require('fs-extra'); @@ -19,6 +19,7 @@ class Command extends BaseCommand { let title = args['note']; let note = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() }); + this.encryptionCheck(note); if (!note) throw new Error(_('Cannot find "%s".', title)); const localFilePath = args['file']; diff --git a/CliClient/app/command-cat.js b/CliClient/app/command-cat.js index 15db03fe0..04237b681 100644 --- a/CliClient/app/command-cat.js +++ b/CliClient/app/command-cat.js @@ -1,9 +1,9 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); +const BaseModel = require('lib/BaseModel.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); class Command extends BaseCommand { @@ -21,10 +21,6 @@ class Command extends BaseCommand { ]; } - enabled() { - return false; - } - async action(args) { let title = args['note']; @@ -33,6 +29,9 @@ class Command extends BaseCommand { const content = args.options.verbose ? await Note.serialize(item) : await Note.serializeForEdit(item); this.stdout(content); + + app().gui().showConsole(); + app().gui().maximizeConsole(); } } diff --git a/CliClient/app/command-config.js b/CliClient/app/command-config.js index c72967548..15a3d484a 100644 --- a/CliClient/app/command-config.js +++ b/CliClient/app/command-config.js @@ -1,7 +1,7 @@ const { BaseCommand } = require('./base-command.js'); const { _, setLocale } = require('lib/locale.js'); const { app } = require('./app.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); class Command extends BaseCommand { diff --git a/CliClient/app/command-cp.js b/CliClient/app/command-cp.js index 9bef2918b..05d2938e0 100644 --- a/CliClient/app/command-cp.js +++ b/CliClient/app/command-cp.js @@ -1,9 +1,9 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); +const BaseModel = require('lib/BaseModel.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); class Command extends BaseCommand { diff --git a/CliClient/app/command-done.js b/CliClient/app/command-done.js index 616cf77f9..b05f3f55b 100644 --- a/CliClient/app/command-done.js +++ b/CliClient/app/command-done.js @@ -1,9 +1,9 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); +const BaseModel = require('lib/BaseModel.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); const { time } = require('lib/time-utils.js'); class Command extends BaseCommand { @@ -16,8 +16,9 @@ class Command extends BaseCommand { return _('Marks a to-do as done.'); } - static async handleAction(args, isCompleted) { + static async handleAction(commandInstance, args, isCompleted) { const note = await app().loadItem(BaseModel.TYPE_NOTE, args.note); + commandInstance.encryptionCheck(note); if (!note) throw new Error(_('Cannot find "%s".', args.note)); if (!note.is_todo) throw new Error(_('Note is not a to-do: "%s"', args.note)); @@ -32,7 +33,7 @@ class Command extends BaseCommand { } async action(args) { - await Command.handleAction(args, true); + await Command.handleAction(this, args, true); } } diff --git a/CliClient/app/command-dump.js b/CliClient/app/command-dump.js index 61ec789b2..0672c2632 100644 --- a/CliClient/app/command-dump.js +++ b/CliClient/app/command-dump.js @@ -1,9 +1,9 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); -const { Tag } = require('lib/models/tag.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); +const Tag = require('lib/models/Tag.js'); class Command extends BaseCommand { diff --git a/CliClient/app/command-e2ee.js b/CliClient/app/command-e2ee.js new file mode 100644 index 000000000..2bf4c092c --- /dev/null +++ b/CliClient/app/command-e2ee.js @@ -0,0 +1,183 @@ +const { BaseCommand } = require('./base-command.js'); +const { _ } = require('lib/locale.js'); +const { cliUtils } = require('./cli-utils.js'); +const EncryptionService = require('lib/services/EncryptionService'); +const DecryptionWorker = require('lib/services/DecryptionWorker'); +const MasterKey = require('lib/models/MasterKey'); +const BaseItem = require('lib/models/BaseItem'); +const Setting = require('lib/models/Setting.js'); + +class Command extends BaseCommand { + + usage() { + return 'e2ee [path]'; + } + + description() { + return _('Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status` and `target-status`.'); + } + + options() { + return [ + // This is here mostly for testing - shouldn't be used + ['-p, --password ', 'Use this password as master password (For security reasons, it is not recommended to use this option).'], + ['-v, --verbose', 'More verbose output for the `target-status` command'], + ]; + } + + async action(args) { + // change-password + + const options = args.options; + + if (args.command === 'enable') { + const password = options.password ? options.password.toString() : await this.prompt(_('Enter master password:'), { type: 'string', secure: true }); + if (!password) { + this.stdout(_('Operation cancelled')); + return; + } + + await EncryptionService.instance().generateMasterKeyAndEnableEncryption(password); + return; + } + + if (args.command === 'disable') { + await EncryptionService.instance().disableEncryption(); + return; + } + + if (args.command === 'decrypt') { + this.stdout(_('Starting decryption... Please wait as it may take several minutes depending on how much there is to decrypt.')); + + while (true) { + try { + await DecryptionWorker.instance().start(); + break; + } catch (error) { + if (error.code === 'masterKeyNotLoaded') { + const masterKeyId = error.masterKeyId; + const password = await this.prompt(_('Enter master password:'), { type: 'string', secure: true }); + if (!password) { + this.stdout(_('Operation cancelled')); + return; + } + Setting.setObjectKey('encryption.passwordCache', masterKeyId, password); + await EncryptionService.instance().loadMasterKeysFromSettings(); + continue; + } + + throw error; + } + } + + this.stdout(_('Completed decryption.')); + + return; + } + + if (args.command === 'status') { + this.stdout(_('Encryption is: %s', Setting.value('encryption.enabled') ? _('Enabled') : _('Disabled'))); + return; + } + + if (args.command === 'target-status') { + const fs = require('fs-extra'); + const pathUtils = require('lib/path-utils.js'); + const fsDriver = new (require('lib/fs-driver-node.js').FsDriverNode)(); + + const targetPath = args.path; + if (!targetPath) throw new Error('Please specify the sync target path.'); + + const dirPaths = function(targetPath) { + let paths = []; + fs.readdirSync(targetPath).forEach((path) => { + paths.push(path); + }); + return paths; + } + + let itemCount = 0; + let resourceCount = 0; + let encryptedItemCount = 0; + let encryptedResourceCount = 0; + let otherItemCount = 0; + + let encryptedPaths = []; + let decryptedPaths = []; + + let paths = dirPaths(targetPath); + + for (let i = 0; i < paths.length; i++) { + const path = paths[i]; + const fullPath = targetPath + '/' + path; + const stat = await fs.stat(fullPath); + + // this.stdout(fullPath); + + if (path === '.resource') { + let resourcePaths = dirPaths(fullPath); + for (let j = 0; j < resourcePaths.length; j++) { + const resourcePath = resourcePaths[j]; + resourceCount++; + const fullResourcePath = fullPath + '/' + resourcePath; + const isEncrypted = await EncryptionService.instance().fileIsEncrypted(fullResourcePath); + if (isEncrypted) { + encryptedResourceCount++; + encryptedPaths.push(fullResourcePath); + } else { + decryptedPaths.push(fullResourcePath); + } + } + } else if (stat.isDirectory()) { + continue; + } else { + itemCount++; + const content = await fs.readFile(fullPath, 'utf8'); + const item = await BaseItem.unserialize(content); + const ItemClass = BaseItem.itemClass(item); + + if (!ItemClass.encryptionSupported()) { + otherItemCount++; + continue; + } + + const isEncrypted = await EncryptionService.instance().itemIsEncrypted(item); + + if (isEncrypted) { + encryptedItemCount++; + encryptedPaths.push(fullPath); + } else { + decryptedPaths.push(fullPath); + } + } + } + + this.stdout('Encrypted items: ' + encryptedItemCount + '/' + itemCount); + this.stdout('Encrypted resources: ' + encryptedResourceCount + '/' + resourceCount); + this.stdout('Other items (never encrypted): ' + otherItemCount); + + if (options.verbose) { + this.stdout(''); + this.stdout('# Encrypted paths'); + this.stdout(''); + for (let i = 0; i < encryptedPaths.length; i++) { + const path = encryptedPaths[i]; + this.stdout(path); + } + + this.stdout(''); + this.stdout('# Decrypted paths'); + this.stdout(''); + for (let i = 0; i < decryptedPaths.length; i++) { + const path = decryptedPaths[i]; + this.stdout(path); + } + } + + return; + } + } + +} + +module.exports = Command; \ No newline at end of file diff --git a/CliClient/app/command-edit.js b/CliClient/app/command-edit.js index 9fc7fec7e..2df24cddc 100644 --- a/CliClient/app/command-edit.js +++ b/CliClient/app/command-edit.js @@ -3,10 +3,10 @@ const { BaseCommand } = require('./base-command.js'); const { uuid } = require('lib/uuid.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); -const { Setting } = require('lib/models/setting.js'); -const { BaseModel } = require('lib/base-model.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); +const Setting = require('lib/models/Setting.js'); +const BaseModel = require('lib/BaseModel.js'); const { cliUtils } = require('./cli-utils.js'); const { time } = require('lib/time-utils.js'); @@ -44,6 +44,8 @@ class Command extends BaseCommand { if (!app().currentFolder()) throw new Error(_('No active notebook.')); let note = await app().loadItem(BaseModel.TYPE_NOTE, title); + this.encryptionCheck(note); + if (!note) { const ok = await this.prompt(_('Note does not exist: "%s". Create it?', title)); if (!ok) return; @@ -76,12 +78,12 @@ class Command extends BaseCommand { app().gui().showModalOverlay(_('Starting to edit note. Close the editor to get back to the prompt.')); await app().gui().forceRender(); - const termState = app().gui().term().saveState(); + const termState = app().gui().termSaveState(); const spawnSync = require('child_process').spawnSync; spawnSync(editorPath, editorArgs, { stdio: 'inherit' }); - app().gui().term().restoreState(termState); + app().gui().termRestoreState(termState); app().gui().hideModalOverlay(); app().gui().forceRender(); diff --git a/CliClient/app/command-export-sync-status.js b/CliClient/app/command-export-sync-status.js index c8ecd7328..5b900c414 100644 --- a/CliClient/app/command-export-sync-status.js +++ b/CliClient/app/command-export-sync-status.js @@ -1,7 +1,7 @@ const { BaseCommand } = require('./base-command.js'); const { Database } = require('lib/database.js'); const { app } = require('./app.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { _ } = require('lib/locale.js'); const { ReportService } = require('lib/services/report.js'); const fs = require('fs-extra'); diff --git a/CliClient/app/command-export.js b/CliClient/app/command-export.js index 798e3dc0c..ce6d70334 100644 --- a/CliClient/app/command-export.js +++ b/CliClient/app/command-export.js @@ -1,7 +1,7 @@ const { BaseCommand } = require('./base-command.js'); const { Exporter } = require('lib/services/exporter.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Note } = require('lib/models/note.js'); +const BaseModel = require('lib/BaseModel.js'); +const Note = require('lib/models/Note.js'); const { reg } = require('lib/registry.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); diff --git a/CliClient/app/command-geoloc.js b/CliClient/app/command-geoloc.js index 32aa9078f..eddb3bf69 100644 --- a/CliClient/app/command-geoloc.js +++ b/CliClient/app/command-geoloc.js @@ -1,9 +1,9 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); +const BaseModel = require('lib/BaseModel.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); class Command extends BaseCommand { diff --git a/CliClient/app/command-help.js b/CliClient/app/command-help.js index 78afe7c1e..a46e60dfa 100644 --- a/CliClient/app/command-help.js +++ b/CliClient/app/command-help.js @@ -2,7 +2,7 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { renderCommandHelp } = require('./help-utils.js'); const { Database } = require('lib/database.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { wrap } = require('lib/string-utils.js'); const { _ } = require('lib/locale.js'); const { cliUtils } = require('./cli-utils.js'); diff --git a/CliClient/app/command-import-enex.js b/CliClient/app/command-import-enex.js index c32d9828b..a1f4dfc80 100644 --- a/CliClient/app/command-import-enex.js +++ b/CliClient/app/command-import-enex.js @@ -1,7 +1,7 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { Folder } = require('lib/models/folder.js'); +const Folder = require('lib/models/Folder.js'); const { importEnex } = require('lib/import-enex'); const { filename, basename } = require('lib/path-utils.js'); const { cliUtils } = require('./cli-utils.js'); diff --git a/CliClient/app/command-ls.js b/CliClient/app/command-ls.js index 5429ba689..0f6525915 100644 --- a/CliClient/app/command-ls.js +++ b/CliClient/app/command-ls.js @@ -1,10 +1,10 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); -const { Setting } = require('lib/models/setting.js'); -const { Note } = require('lib/models/note.js'); +const BaseModel = require('lib/BaseModel.js'); +const Folder = require('lib/models/Folder.js'); +const Setting = require('lib/models/Setting.js'); +const Note = require('lib/models/Note.js'); const { sprintf } = require('sprintf-js'); const { time } = require('lib/time-utils.js'); const { cliUtils } = require('./cli-utils.js'); diff --git a/CliClient/app/command-mkbook.js b/CliClient/app/command-mkbook.js index 68e5df875..831ff4a0f 100644 --- a/CliClient/app/command-mkbook.js +++ b/CliClient/app/command-mkbook.js @@ -1,7 +1,7 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { Folder } = require('lib/models/folder.js'); +const Folder = require('lib/models/Folder.js'); const { reg } = require('lib/registry.js'); class Command extends BaseCommand { diff --git a/CliClient/app/command-mknote.js b/CliClient/app/command-mknote.js index a4d93b3ef..79f4c19a7 100644 --- a/CliClient/app/command-mknote.js +++ b/CliClient/app/command-mknote.js @@ -1,7 +1,7 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { Note } = require('lib/models/note.js'); +const Note = require('lib/models/Note.js'); class Command extends BaseCommand { diff --git a/CliClient/app/command-mktodo.js b/CliClient/app/command-mktodo.js index 6163928ee..d96fb5275 100644 --- a/CliClient/app/command-mktodo.js +++ b/CliClient/app/command-mktodo.js @@ -1,7 +1,7 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { Note } = require('lib/models/note.js'); +const Note = require('lib/models/Note.js'); class Command extends BaseCommand { diff --git a/CliClient/app/command-mv.js b/CliClient/app/command-mv.js index 9bf37a5c8..729bab96e 100644 --- a/CliClient/app/command-mv.js +++ b/CliClient/app/command-mv.js @@ -1,9 +1,9 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); +const BaseModel = require('lib/BaseModel.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); class Command extends BaseCommand { diff --git a/CliClient/app/command-ren.js b/CliClient/app/command-ren.js index 2e09a5438..45b60671c 100644 --- a/CliClient/app/command-ren.js +++ b/CliClient/app/command-ren.js @@ -1,9 +1,9 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); +const BaseModel = require('lib/BaseModel.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); class Command extends BaseCommand { @@ -20,6 +20,7 @@ class Command extends BaseCommand { const name = args['name']; const item = await app().loadItem('folderOrNote', pattern); + this.encryptionCheck(item); if (!item) throw new Error(_('Cannot find "%s".', pattern)); const newItem = { diff --git a/CliClient/app/command-rmbook.js b/CliClient/app/command-rmbook.js index 545522216..6e9c48474 100644 --- a/CliClient/app/command-rmbook.js +++ b/CliClient/app/command-rmbook.js @@ -1,10 +1,10 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { BaseItem } = require('lib/models/base-item.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); -const { BaseModel } = require('lib/base-model.js'); +const BaseItem = require('lib/models/BaseItem.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); +const BaseModel = require('lib/BaseModel.js'); const { cliUtils } = require('./cli-utils.js'); class Command extends BaseCommand { diff --git a/CliClient/app/command-rmnote.js b/CliClient/app/command-rmnote.js index 8ad657763..8e1ecea2f 100644 --- a/CliClient/app/command-rmnote.js +++ b/CliClient/app/command-rmnote.js @@ -1,10 +1,10 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { BaseItem } = require('lib/models/base-item.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); -const { BaseModel } = require('lib/base-model.js'); +const BaseItem = require('lib/models/BaseItem.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); +const BaseModel = require('lib/BaseModel.js'); const { cliUtils } = require('./cli-utils.js'); class Command extends BaseCommand { diff --git a/CliClient/app/command-search.js b/CliClient/app/command-search.js index 24031aee4..3ae14ea2c 100644 --- a/CliClient/app/command-search.js +++ b/CliClient/app/command-search.js @@ -1,9 +1,9 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); +const BaseModel = require('lib/BaseModel.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); const { sprintf } = require('sprintf-js'); const { time } = require('lib/time-utils.js'); const { uuid } = require('lib/uuid.js'); diff --git a/CliClient/app/command-set.js b/CliClient/app/command-set.js index 391887523..3a01ae5b4 100644 --- a/CliClient/app/command-set.js +++ b/CliClient/app/command-set.js @@ -1,11 +1,11 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { BaseModel } = require('lib/base-model.js'); +const BaseModel = require('lib/BaseModel.js'); const { Database } = require('lib/database.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); -const { BaseItem } = require('lib/models/base-item.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); +const BaseItem = require('lib/models/BaseItem.js'); class Command extends BaseCommand { @@ -35,6 +35,8 @@ class Command extends BaseCommand { if (!notes.length) throw new Error(_('Cannot find "%s".', title)); for (let i = 0; i < notes.length; i++) { + this.encryptionCheck(notes[i]); + let newNote = { id: notes[i].id, type_: notes[i].type_, diff --git a/CliClient/app/command-status.js b/CliClient/app/command-status.js index 1b252e66f..0bda410bf 100644 --- a/CliClient/app/command-status.js +++ b/CliClient/app/command-status.js @@ -1,7 +1,7 @@ const { BaseCommand } = require('./base-command.js'); const { Database } = require('lib/database.js'); const { app } = require('./app.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { _ } = require('lib/locale.js'); const { ReportService } = require('lib/services/report.js'); diff --git a/CliClient/app/command-sync.js b/CliClient/app/command-sync.js index 3df132722..88a3cb6f7 100644 --- a/CliClient/app/command-sync.js +++ b/CliClient/app/command-sync.js @@ -2,8 +2,8 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); const { OneDriveApiNodeUtils } = require('./onedrive-api-node-utils.js'); -const { Setting } = require('lib/models/setting.js'); -const { BaseItem } = require('lib/models/base-item.js'); +const Setting = require('lib/models/Setting.js'); +const BaseItem = require('lib/models/BaseItem.js'); const { Synchronizer } = require('lib/synchronizer.js'); const { reg } = require('lib/registry.js'); const { cliUtils } = require('./cli-utils.js'); diff --git a/CliClient/app/command-tag.js b/CliClient/app/command-tag.js index e31bf2330..f2a38943c 100644 --- a/CliClient/app/command-tag.js +++ b/CliClient/app/command-tag.js @@ -1,8 +1,8 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { Tag } = require('lib/models/tag.js'); -const { BaseModel } = require('lib/base-model.js'); +const Tag = require('lib/models/Tag.js'); +const BaseModel = require('lib/BaseModel.js'); class Command extends BaseCommand { diff --git a/CliClient/app/command-todo.js b/CliClient/app/command-todo.js index d31b7faeb..1f83e2cda 100644 --- a/CliClient/app/command-todo.js +++ b/CliClient/app/command-todo.js @@ -1,9 +1,9 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); +const BaseModel = require('lib/BaseModel.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); const { time } = require('lib/time-utils.js'); class Command extends BaseCommand { @@ -25,6 +25,8 @@ class Command extends BaseCommand { for (let i = 0; i < notes.length; i++) { const note = notes[i]; + this.encryptionCheck(note); + let toSave = { id: note.id, }; diff --git a/CliClient/app/command-undone.js b/CliClient/app/command-undone.js index 282a25dd5..3373c0ee3 100644 --- a/CliClient/app/command-undone.js +++ b/CliClient/app/command-undone.js @@ -1,9 +1,9 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); +const BaseModel = require('lib/BaseModel.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); const { time } = require('lib/time-utils.js'); const CommandDone = require('./command-done.js'); @@ -19,7 +19,7 @@ class Command extends BaseCommand { } async action(args) { - await CommandDone.handleAction(args, false); + await CommandDone.handleAction(this, args, false); } } diff --git a/CliClient/app/command-use.js b/CliClient/app/command-use.js index ea27bc861..c25a635c2 100644 --- a/CliClient/app/command-use.js +++ b/CliClient/app/command-use.js @@ -1,8 +1,8 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); +const BaseModel = require('lib/BaseModel.js'); +const Folder = require('lib/models/Folder.js'); class Command extends BaseCommand { diff --git a/CliClient/app/command-version.js b/CliClient/app/command-version.js index cfb9bc11a..9a3603b24 100644 --- a/CliClient/app/command-version.js +++ b/CliClient/app/command-version.js @@ -1,5 +1,5 @@ const { BaseCommand } = require('./base-command.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { _ } = require('lib/locale.js'); class Command extends BaseCommand { diff --git a/CliClient/app/fuzzing.js b/CliClient/app/fuzzing.js index 261678760..f28fb0c06 100644 --- a/CliClient/app/fuzzing.js +++ b/CliClient/app/fuzzing.js @@ -2,7 +2,7 @@ const { time } = require('lib/time-utils.js'); const { Logger } = require('lib/logger.js'); -const { Resource } = require('lib/models/resource.js'); +const Resource = require('lib/models/Resource.js'); const { dirname } = require('lib/path-utils.js'); const { FsDriverNode } = require('./fs-driver-node.js'); const lodash = require('lodash'); diff --git a/CliClient/app/gui/FolderListWidget.js b/CliClient/app/gui/FolderListWidget.js index 8b9f5e15e..4532d1a7c 100644 --- a/CliClient/app/gui/FolderListWidget.js +++ b/CliClient/app/gui/FolderListWidget.js @@ -1,6 +1,6 @@ -const Folder = require('lib/models/folder.js').Folder; -const Tag = require('lib/models/tag.js').Tag; -const BaseModel = require('lib/base-model.js').BaseModel; +const Folder = require('lib/models/Folder.js'); +const Tag = require('lib/models/Tag.js'); +const BaseModel = require('lib/BaseModel.js'); const ListWidget = require('tkwidgets/ListWidget.js'); const _ = require('lib/locale.js')._; @@ -24,9 +24,9 @@ class FolderListWidget extends ListWidget { if (item === '-') { output.push('-'.repeat(this.innerWidth)); } else if (item.type_ === Folder.modelType()) { - output.push(item.title); + output.push(Folder.displayTitle(item)); } else if (item.type_ === Tag.modelType()) { - output.push('[' + item.title + ']'); + output.push('[' + Folder.displayTitle(item) + ']'); } else if (item.type_ === BaseModel.TYPE_SEARCH) { output.push(_('Search:')); output.push(item.title); diff --git a/CliClient/app/gui/NoteListWidget.js b/CliClient/app/gui/NoteListWidget.js index 81341d25c..30d44df44 100644 --- a/CliClient/app/gui/NoteListWidget.js +++ b/CliClient/app/gui/NoteListWidget.js @@ -1,4 +1,4 @@ -const Note = require('lib/models/note.js').Note; +const Note = require('lib/models/Note.js'); const ListWidget = require('tkwidgets/ListWidget.js'); class NoteListWidget extends ListWidget { @@ -10,7 +10,7 @@ class NoteListWidget extends ListWidget { this.updateIndexFromSelectedNoteId_ = false; this.itemRenderer = (note) => { - let label = note.title; // + ' ' + note.id; + let label = Note.displayTitle(note); // + ' ' + note.id; if (note.is_todo) { label = '[' + (note.todo_completed ? 'X' : ' ') + '] ' + label; } diff --git a/CliClient/app/gui/NoteMetadataWidget.js b/CliClient/app/gui/NoteMetadataWidget.js index fc29e04f2..ff68e189d 100644 --- a/CliClient/app/gui/NoteMetadataWidget.js +++ b/CliClient/app/gui/NoteMetadataWidget.js @@ -1,4 +1,4 @@ -const Note = require('lib/models/note.js').Note; +const Note = require('lib/models/Note.js'); const TextWidget = require('tkwidgets/TextWidget.js'); class NoteMetadataWidget extends TextWidget { diff --git a/CliClient/app/gui/NoteWidget.js b/CliClient/app/gui/NoteWidget.js index 991f1dd32..d0a29538e 100644 --- a/CliClient/app/gui/NoteWidget.js +++ b/CliClient/app/gui/NoteWidget.js @@ -1,4 +1,4 @@ -const Note = require('lib/models/note.js').Note; +const Note = require('lib/models/Note.js'); const TextWidget = require('tkwidgets/TextWidget.js'); const { _ } = require('lib/locale.js'); @@ -44,7 +44,13 @@ class NoteWidget extends TextWidget { } else if (this.noteId_) { this.doAsync('loadNote', async () => { this.note_ = await Note.load(this.noteId_); - this.text = this.note_ ? this.note_.title + "\n\n" + this.note_.body : ''; + + if (this.note_ && this.note_.encryption_applied) { + this.text = _('One or more items are currently encrypted and you may need to supply a master password. To do so please type `e2ee decrypt`. If you have already supplied the password, the encrypted items are being decrypted in the background and will be available soon.'); + } else { + this.text = this.note_ ? this.note_.title + "\n\n" + this.note_.body : ''; + } + if (this.lastLoadedNoteId_ !== this.noteId_) this.scrollTop = 0; this.lastLoadedNoteId_ = this.noteId_; }); diff --git a/CliClient/app/gui/StatusBarWidget.js b/CliClient/app/gui/StatusBarWidget.js index 9b339a5a9..8a3da2bf3 100644 --- a/CliClient/app/gui/StatusBarWidget.js +++ b/CliClient/app/gui/StatusBarWidget.js @@ -2,6 +2,7 @@ const BaseWidget = require('tkwidgets/BaseWidget.js'); const chalk = require('chalk'); const termutils = require('tkwidgets/framework/termutils.js'); const stripAnsi = require('strip-ansi'); +const { handleAutocompletion } = require('../autocompletion.js'); class StatusBarWidget extends BaseWidget { @@ -41,6 +42,7 @@ class StatusBarWidget extends BaseWidget { }; if ('cursorPosition' in options) this.promptState_.cursorPosition = options.cursorPosition; + if ('secure' in options) this.promptState_.secure = options.secure; this.promptState_.promise = new Promise((resolve, reject) => { this.promptState_.resolve = resolve; @@ -104,13 +106,19 @@ class StatusBarWidget extends BaseWidget { this.term.showCursor(true); + const isSecurePrompt = !!this.promptState_.secure; + let options = { cancelable: true, history: this.history, default: this.promptState_.initialText, + autoComplete: handleAutocompletion, + autoCompleteHint : true, + autoCompleteMenu : true, }; if ('cursorPosition' in this.promptState_) options.cursorPosition = this.promptState_.cursorPosition; + if (isSecurePrompt) options.echoChar = true; this.inputEventEmitter_ = this.term.inputField(options, (error, input) => { let resolveResult = null; @@ -125,7 +133,7 @@ class StatusBarWidget extends BaseWidget { resolveResult = input ? input.trim() : input; // Add the command to history but only if it's longer than one character. // Below that it's usually an answer like "y"/"n", etc. - if (input && input.length > 1) this.history_.push(input); + if (!isSecurePrompt && input && input.length > 1) this.history_.push(input); } } @@ -159,4 +167,4 @@ class StatusBarWidget extends BaseWidget { } -module.exports = StatusBarWidget; \ No newline at end of file +module.exports = StatusBarWidget; diff --git a/CliClient/app/help-utils.js b/CliClient/app/help-utils.js index b45d4d0d1..5e9a76f95 100644 --- a/CliClient/app/help-utils.js +++ b/CliClient/app/help-utils.js @@ -1,6 +1,6 @@ const fs = require('fs-extra'); const { wrap } = require('lib/string-utils.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { fileExtension, basename, dirname } = require('lib/path-utils.js'); const { _, setLocale, languageCode } = require('lib/locale.js'); diff --git a/CliClient/app/main.js b/CliClient/app/main.js index 3ddfe5a95..accf86072 100644 --- a/CliClient/app/main.js +++ b/CliClient/app/main.js @@ -1,26 +1,27 @@ #!/usr/bin/env node -// Loading time: 20170803: 1.5s with no commands - +// Make it possible to require("/lib/...") without specifying full path require('app-module-path').addPath(__dirname); const { app } = require('./app.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); -const { Resource } = require('lib/models/resource.js'); -const { BaseItem } = require('lib/models/base-item.js'); -const { Note } = require('lib/models/note.js'); -const { Tag } = require('lib/models/tag.js'); -const { NoteTag } = require('lib/models/note-tag.js'); -const { Setting } = require('lib/models/setting.js'); +const Folder = require('lib/models/Folder.js'); +const Resource = require('lib/models/Resource.js'); +const BaseItem = require('lib/models/BaseItem.js'); +const Note = require('lib/models/Note.js'); +const Tag = require('lib/models/Tag.js'); +const NoteTag = require('lib/models/NoteTag.js'); +const MasterKey = require('lib/models/MasterKey'); +const Setting = require('lib/models/Setting.js'); const { Logger } = require('lib/logger.js'); const { FsDriverNode } = require('lib/fs-driver-node.js'); const { shimInit } = require('lib/shim-init-node.js'); const { _ } = require('lib/locale.js'); +const EncryptionService = require('lib/services/EncryptionService'); const fsDriver = new FsDriverNode(); Logger.fsDriver_ = fsDriver; Resource.fsDriver_ = fsDriver; +EncryptionService.fsDriver_ = fsDriver; // That's not good, but it's to avoid circular dependency issues // in the BaseItem class. @@ -29,6 +30,7 @@ BaseItem.loadClass('Folder', Folder); BaseItem.loadClass('Resource', Resource); BaseItem.loadClass('Tag', Tag); BaseItem.loadClass('NoteTag', NoteTag); +BaseItem.loadClass('MasterKey', MasterKey); Setting.setConstant('appId', 'net.cozic.joplin-cli'); Setting.setConstant('appType', 'cli'); diff --git a/CliClient/build.sh b/CliClient/build.sh index bf2394366..2f587e0f0 100755 --- a/CliClient/build.sh +++ b/CliClient/build.sh @@ -4,6 +4,6 @@ ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" BUILD_DIR="$ROOT_DIR/build" rsync -a --exclude "node_modules/" "$ROOT_DIR/app/" "$BUILD_DIR/" -rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/" +rsync -a --delete "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/" cp "$ROOT_DIR/package.json" "$BUILD_DIR" chmod 755 "$BUILD_DIR/main.js" \ No newline at end of file diff --git a/CliClient/locales/de_DE.po b/CliClient/locales/de_DE.po index 103f2a205..4d9163882 100644 --- a/CliClient/locales/de_DE.po +++ b/CliClient/locales/de_DE.po @@ -2,18 +2,18 @@ # Copyright (C) YEAR Laurent Cozic # This file is distributed under the same license as the Joplin-CLI package. # FIRST AUTHOR , YEAR. -# +# msgid "" msgstr "" "Project-Id-Version: Joplin-CLI 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: \n" +"Last-Translator: Samuel Blickle \n" "Language-Team: \n" "Language: de_DE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 2.0.4\n" +"X-Generator: Poedit 2.0.5\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Give focus to next pane" @@ -38,7 +38,7 @@ msgid "Exit the application." msgstr "Das Programm verlassen." msgid "Delete the currently selected note or notebook." -msgstr "Die momentan ausgewählte Notiz(-buch) löschen." +msgstr "Die/das momentan ausgewählte Notiz(-buch) löschen." msgid "To delete a tag, untag the associated notes." msgstr "" @@ -79,7 +79,7 @@ msgid "Move the note to a notebook." msgstr "Die Notiz zu einem Notizbuch verschieben." msgid "Press Ctrl+D or type \"exit\" to exit the application" -msgstr "Drücke Strg+D oder schreibe \"exit\", um das Programm zu verlassen" +msgstr "Drücke Strg+D oder tippe \"exit\", um das Programm zu verlassen" #, javascript-format msgid "More than one item match \"%s\". Please narrow down your query." @@ -106,11 +106,11 @@ msgid "y" msgstr "j" msgid "Cancelling background synchronisation... Please wait." -msgstr "Breche Hintergrund-Synchronisations ab....Bitte warten." +msgstr "Breche Hintergrund-Synchronisation ab... Bitte warten." -#, fuzzy, javascript-format +#, javascript-format msgid "No such command: %s" -msgstr "Ungültiger Befehl: \"%s\"" +msgstr "Ungültiger Befehl: %s" #, javascript-format msgid "The command \"%s\" is only available in GUI mode" @@ -150,12 +150,12 @@ msgid "" "current configuration." msgstr "" "Zeigt an oder stellt einen Optionswert. Wenn kein [Wert] angegeben ist, wird " -"der Wert vom gegebenenen [Namen] angezeigt. Wenn weder [Name] noch [Wert] " +"der Wert vom gegebenen [Namen] angezeigt. Wenn weder [Name] noch [Wert] " "gegeben sind, wird eine Liste der momentanen Konfiguration angezeigt." msgid "Also displays unset and hidden config variables." msgstr "" -"Zeige auch nicht angegebene oder versteckte Konfigurationsvariablen an." +"Zeigt auch nicht angegebene oder versteckte Konfigurationsvariablen an." #, javascript-format msgid "%s = %s (%s)" @@ -169,8 +169,8 @@ msgid "" "Duplicates the notes matching to [notebook]. If no notebook is " "specified the note is duplicated in the current notebook." msgstr "" -"Vervielfältigt die Notizen die mit übereinstimmen zu [Notizbuch]. " -"Wenn kein Notizbuch angegeben ist, wird die Notiz in das momentane Notizbuch " +"Dupliziert die Notizen die mit übereinstimmen zu [Notizbuch]. Wenn " +"kein Notizbuch angegeben ist, wird die Notiz in das momentane Notizbuch " "kopiert." msgid "Marks a to-do as done." @@ -186,8 +186,8 @@ msgstr "Notiz bearbeiten." msgid "" "No text editor is defined. Please set it using `config editor `" msgstr "" -"Kein Textbearbeitungsprogramm angegeben. Bitte lege eines mit `config editor " -"` fest" +"Kein Textverarbeitungsprogramm angegeben. Bitte lege eines mit `config " +"editor ` fest" msgid "No active notebook." msgstr "Kein aktives Notizbuch." @@ -198,8 +198,8 @@ msgstr "Notiz \"%s\" existiert nicht. Soll sie erstellt werden?" msgid "Starting to edit note. Close the editor to get back to the prompt." msgstr "" -"Beginne die Notiz zu bearbeiten. Schließ das Textbearbeitungsprogramm, um " -"zurück zum Terminal zu kommen." +"Beginne die Notiz zu bearbeiten. Schließe das Textverarbeitungsprogramm, um " +"zurück zum Terminal zu gelangen." msgid "Note has been saved." msgstr "Die Notiz wurde gespeichert." @@ -211,9 +211,9 @@ msgid "" "Exports Joplin data to the given directory. By default, it will export the " "complete database including notebooks, notes, tags and resources." msgstr "" -"Exportiert Joplins Datein zu dem angegebenen Pfad. Standardmäßig wird die " -"komplette Datenbank inklusive Notizbüchern, Notizen, Markierungen usw. " -"exportiert." +"Exportiert Joplins Dateien zu dem angegebenen Pfad. Standardmäßig wird die " +"komplette Datenbank inklusive Notizbüchern, Notizen, Markierungen und " +"Anhängen exportiert." msgid "Exports only the given note." msgstr "Exportiert nur die angegebene Notiz." @@ -225,10 +225,10 @@ msgid "Displays a geolocation URL for the note." msgstr "Zeigt die Standort-URL der Notiz an." msgid "Displays usage information." -msgstr "Zeigt die Benutzungsstatistik an." +msgstr "Zeigt die Nutzungsstatistik an." msgid "Shortcuts are not available in CLI mode." -msgstr "" +msgstr "Tastenkürzel sind im CLI Modus nicht verfügbar." #, fuzzy msgid "" @@ -248,8 +248,8 @@ msgid "" msgstr "" "In jedem Befehl können Notizen oder Notizbücher durch ihren Titel oder ihre " "ID spezifiziert werden, oder durch die Abkürzung `$n` oder `$b` um entweder " -"das momentan augewählte Notizbuch oder die momentan ausgewählte Notiz zu " -"wählen. `$c` kann benutzt werden, um die momentane Auswahl zu verweisen." +"das momentan ausgewählte Notizbuch oder die momentan ausgewählte Notiz zu " +"wählen. `$c` kann benutzt werden, um auf die momentane Auswahl zu verweisen." msgid "To move from one pane to another, press Tab or Shift+Tab." msgstr "" @@ -261,16 +261,16 @@ msgid "" "(including this console)." msgstr "" "Benutze die Pfeiltasten und Bild hoch/runter um durch Listen und Texte zu " -"scrollen ( inklusive diesem Terminal )." +"scrollen (inklusive diesem Terminal)." msgid "To maximise/minimise the console, press \"TC\"." msgstr "Um das Terminal zu maximieren/minimieren, drücke \"TC\"." msgid "To enter command line mode, press \":\"" -msgstr "" +msgstr "Um den Kommandozeilen Modus aufzurufen, drücke \":\"" msgid "To exit command line mode, press ESCAPE" -msgstr "" +msgstr "Um den Kommandozeilen Modus zu beenden, drücke ESCAPE" msgid "" "For the complete list of available keyboard shortcuts, type `help shortcuts`" @@ -287,7 +287,7 @@ msgstr "Nicht nach einer Bestätigung fragen." #, javascript-format msgid "File \"%s\" will be imported into existing notebook \"%s\". Continue?" msgstr "" -"Datei \"%s\" wird importiert in das existierende Notizbuch \"%s\". " +"Datei \"%s\" wird in das existierende Notizbuch \"%s\" importiert. " "Fortfahren?" #, javascript-format @@ -295,7 +295,7 @@ msgid "" "New notebook \"%s\" will be created and file \"%s\" will be imported into " "it. Continue?" msgstr "" -"Ein neues Notizbuch \"%s\" wird erstellt und die Datei \"%s\" wird in es " +"Neues Notizbuch \"%s\" wird erstellt und die Datei \"%s\" wird hinein " "importiert. Fortfahren?" #, javascript-format @@ -316,7 +316,7 @@ msgstr "Übersprungen: %d." #, javascript-format msgid "Resources: %d." -msgstr "" +msgstr "Anhänge: %d." #, javascript-format msgid "Tagged: %d." @@ -337,11 +337,12 @@ msgstr "" "aller Notizbücher anzuzeigen." msgid "Displays only the first top notes." -msgstr "Zeigt nur die Top- Notizen an." +msgstr "Zeigt nur die ersten Notizen an." -#, fuzzy msgid "Sorts the item by (eg. title, updated_time, created_time)." -msgstr "Sortiert nach ( z.B. Titel," +msgstr "" +"Sortiert nach ( z.B. Titel, Bearbeitungszeitpunkt, " +"Erstellungszeitpunkt)" msgid "Reverses the sorting order." msgstr "Dreht die Sortierreihenfolge um." @@ -351,9 +352,9 @@ msgid "" "for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the " "to-dos, while `-ttd` would display notes and to-dos." msgstr "" -"Zeige nur bestimmt Typen an. Kann `n` für Notizen sein, `t` für To-Dos, oder " -"`nt` für Notizen und To-Dos ( z.B. würde `-tt` nur To-Dos anzeigen, während " -"`-ttd` Notizen und To-Dos anzeigen würde )." +"Zeigt nur bestimmte Item Typen an. Kann `n` für Notizen sein, `t` für To-" +"Dos, oder `nt` für Notizen und To-Dos ( z.B. zeigt `-tt` nur To-Dos an, " +"während `-ttd` Notizen und To-Dos anzeigt)." msgid "Either \"text\" or \"json\"" msgstr "Entweder \"text\" oder \"json\"" @@ -362,6 +363,8 @@ msgid "" "Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, " "TODO_CHECKED (for to-dos), TITLE" msgstr "" +"Verwende ausführliches Listen Format. Das Format lautet: ID, NOTIZEN_ANZAHL " +"(für Notizbuch), DATUM, TODO_BEARBEITET (für To-Dos), TITEL" msgid "Please select a notebook first." msgstr "Bitte wähle erst ein Notizbuch aus." @@ -382,16 +385,17 @@ msgid "Moves the notes matching to [notebook]." msgstr "Verschiebt die Notizen, die mit übereinstimmen, zu [Notizbuch]" msgid "Renames the given (note or notebook) to ." -msgstr "Benennt das gegebene ( Notiz oder Notizbuch ) zu um." +msgstr "Benennt das angegebene ( Notiz oder Notizbuch ) zu um." msgid "Deletes the given notebook." -msgstr "Löscht das gegebene Notizbuch." +msgstr "Löscht das ausgewählte Notizbuch." msgid "Deletes the notebook without asking for confirmation." msgstr "Löscht das Notizbuch, ohne nach einer Bestätigung zu fragen." msgid "Delete notebook? All notes within this notebook will also be deleted." msgstr "" +"Notizbuch wirklich löschen? Alle Notizen darin werden ebenfalls gelöscht." msgid "Deletes the notes matching ." msgstr "Löscht die Notizen, die mit übereinstimmen." @@ -408,27 +412,29 @@ msgid "Delete note?" msgstr "Notiz löschen?" msgid "Searches for the given in all the notes." -msgstr "Sucht nach dem gegebenen in allen Notizen." +msgstr "Sucht nach dem angegebenen in allen Notizen." -#, fuzzy, javascript-format +#, javascript-format msgid "" "Sets the property of the given to the given [value]. Possible " "properties are:\n" "\n" "%s" msgstr "" -"Setzt die Eigenschaft der gegebenen zu dem gegebenen [Wert]." +"Setzt die Eigenschaft der gegebenen auf den gegebenen [Wert]. " +"Mögliche Werte sind:\n" +"\n" +"%s" msgid "Displays summary about the notes and notebooks." -msgstr "Zeigt eine Zusammenfassung über die Notizen und Notizbücher an." +msgstr "Zeigt eine Zusammenfassung der Notizen und Notizbücher an." -#, fuzzy msgid "Synchronises with remote storage." -msgstr "Synchronisiert mit " +msgstr "Synchronisiert mit Remotespeicher." msgid "Sync to provided target (defaults to sync.target config value)" msgstr "" -"Mit dem gegebenen Ziel synchronisieren ( voreingestellt auf den sync.target " +"Mit dem angegebenen Ziel synchronisieren (voreingestellt auf den sync.target " "Optionswert)" msgid "Synchronisation is already in progress." @@ -442,12 +448,12 @@ msgid "" msgstr "" "Eine Sperrdatei ist vorhanden. Wenn du dir sicher bist, dass keine " "Synchronisation im Gange ist, kannst du die Sperrdatei \"%s\" löschen und " -"vortfahren." +"fortfahren." msgid "" "Authentication was not completed (did not receive an authentication token)." msgstr "" -"Authentikation wurde nicht abgeschlossen (keinen Authentikations-Token " +"Authentifizierung wurde nicht abgeschlossen (keinen Authentifizierung-Token " "erhalten)." #, javascript-format @@ -478,7 +484,6 @@ msgstr "" msgid "Invalid command: \"%s\"" msgstr "Ungültiger Befehl: \"%s\"" -#, fuzzy msgid "" " can either be \"toggle\" or \"clear\". Use \"toggle\" to " "toggle the given to-do between completed and uncompleted state (If the " @@ -487,8 +492,8 @@ msgid "" msgstr "" " kann entweder \"toggle\" oder \"clear\" sein. Benutze \"toggle" "\", um ein To-Do abzuschließen, oder es zu beginnen (Wenn das Ziel eine " -"normale Notiz ist, wird es zu einem To-Do umgewandelt). Benutze \"clear\", " -"um es zurück zu einem To-Do zu verwandeln." +"normale Notiz ist, wird diese in ein To-Do umgewandelt). Benutze \"clear\", " +"um es zurück in ein To-Do zu verwandeln." msgid "Marks a to-do as non-completed." msgstr "Makiert ein To-Do als nicht-abgeschlossen." @@ -497,8 +502,8 @@ msgid "" "Switches to [notebook] - all further operations will happen within this " "notebook." msgstr "" -"Wechselt zu [Notizbuch] - alle weiteren Tätigkeiten werden in diesem " -"Notizbuch verrichtet." +"Wechselt zu [Notizbuch] - alle weiteren Aktionen werden in diesem Notizbuch " +"ausgeführt." msgid "Displays version information" msgstr "Zeigt die Versionsnummer an" @@ -531,10 +536,10 @@ msgstr "Schwerwiegender Fehler:" msgid "" "The application has been authorised - you may now close this browser tab." msgstr "" -"Das Programm wurde authorisiert - Du kannst nun diesen Browsertab schließen." +"Das Programm wurde autorisiert - Du kannst diesen Browsertab nun schließen." msgid "The application has been successfully authorised." -msgstr "Das Programm wurde erfolgreich authorisiert." +msgstr "Das Programm wurde erfolgreich autorisiert." msgid "" "Please open the following URL in your browser to authenticate the " @@ -546,8 +551,8 @@ msgstr "" "Bitte öffne die folgende URL in deinem Browser, um das Programm zu " "authentifizieren. Das Programm wird einen Ordner in \"Apps/Joplin\" " "erstellen und wird nur in diesem Ordner schreiben und lesen. Es wird weder " -"Zugriff auf Dateien außerhalb dieses Ordners haben, noch auf persönliche " -"Daten. Es werden keine Daten mit Dritten geteilt." +"Zugriff auf Dateien außerhalb dieses Ordners haben, noch auf andere " +"persönliche Daten. Es werden keine Daten mit Dritten geteilt." msgid "Search:" msgstr "Suchen:" @@ -560,6 +565,13 @@ msgid "" "\n" "For example, to create a notebook press `mb`; to create a note press `mn`." msgstr "" +"Willkommen bei Joplin!\n" +"\n" +"Tippe `:help shortcuts` für eine Liste der Shortcuts oder `:help` für " +"Nutzungsinformationen ein.\n" +"\n" +"Um zum Beispiel ein Notizbuch zu erstellen, drücke `mb`; um eine Notiz zu " +"erstellen drücke `mn`." msgid "File" msgstr "Datei" @@ -577,7 +589,7 @@ msgid "Import Evernote notes" msgstr "Evernote Notizen importieren" msgid "Evernote Export Files" -msgstr "" +msgstr "Evernote Export Dateien" msgid "Quit" msgstr "Verlassen" @@ -600,9 +612,8 @@ msgstr "Alle Notizen durchsuchen" msgid "Tools" msgstr "Werkzeuge" -#, fuzzy msgid "Synchronisation status" -msgstr "Synchronisationsziel" +msgstr "Status der Synchronisation" msgid "Options" msgstr "Optionen" @@ -628,11 +639,77 @@ msgstr "Abbrechen" #, javascript-format msgid "Notes and settings are stored in: %s" -msgstr "" +msgstr "Notizen und Einstellungen gespeichert in: %s" msgid "Save" +msgstr "Speichern" + +msgid "" +"Disabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent unencrypted to the sync target. Do you wish to " +"continue?" msgstr "" +msgid "" +"Enabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent encrypted to the sync target. Do not lose the " +"password as, for security purposes, this will be the *only* way to decrypt " +"the data! To enable encryption, please enter your password below." +msgstr "" + +msgid "Disable encryption" +msgstr "" + +msgid "Enable encryption" +msgstr "" + +msgid "Master Keys" +msgstr "" + +msgid "Active" +msgstr "Aktiv" + +msgid "ID" +msgstr "ID" + +msgid "Source" +msgstr "Quelle" + +msgid "Created" +msgstr "Erstellt" + +msgid "Updated" +msgstr "Aktualisiert" + +msgid "Password" +msgstr "Passwort" + +msgid "Password OK" +msgstr "Passwort OK" + +msgid "" +"Note: Only one master key is going to be used for encryption (the one marked " +"as \"active\"). Any of the keys might be used for decryption, depending on " +"how the notes or notebooks were originally encrypted." +msgstr "" +"Hinweis: Nur ein Hauptschlüssel wird für die Verschlüsselung verwendet (der " +"als \"aktiv\" markierte). Jeder der Schlüssel kann für die Entschlüsselung " +"verwendet werden, abhängig davon, wie die jeweiligen Notizen oder " +"Notizbücher ursprünglich verschlüsselt wurden." + +msgid "Status" +msgstr "Status" + +msgid "Encryption is:" +msgstr "" + +#, fuzzy +msgid "Enabled" +msgstr "Deaktiviert" + +msgid "Disabled" +msgstr "Deaktiviert" + msgid "Back" msgstr "Zurück" @@ -640,14 +717,14 @@ msgstr "Zurück" msgid "" "New notebook \"%s\" will be created and file \"%s\" will be imported into it" msgstr "" -"Ein neues Notizbuch \"%s\" wird erstellt und die Datei \"%s\" wird in es " +"Neues Notizbuch \"%s\" wird erstellt und die Datei \"%s\" wird hinein " "importiert" msgid "Please create a notebook first." msgstr "Bitte erstelle zuerst ein Notizbuch." msgid "Note title:" -msgstr "Notiz Titel:" +msgstr "Notizen Titel:" msgid "Please create a notebook first" msgstr "Bitte erstelle zuerst ein Notizbuch" @@ -673,26 +750,18 @@ msgstr "Alarm erstellen:" msgid "Layout" msgstr "Layout" -#, fuzzy msgid "Some items cannot be synchronised." -msgstr "Kann Synchronisierer nicht initialisieren." +msgstr "Manche Objekte können nicht synchronisiert werden." msgid "View them now" -msgstr "" - -msgid "ID" -msgstr "" - -msgid "Source" -msgstr "" +msgstr "Zeige sie jetzt an" #, fuzzy -msgid "Created" -msgstr "Erstellt: %d." +msgid "Some items cannot be decrypted." +msgstr "Kann Synchronisierer nicht initialisieren." -#, fuzzy -msgid "Updated" -msgstr "Aktualisiert: %d." +msgid "Set the password" +msgstr "" msgid "Add or remove tags" msgstr "Markierungen hinzufügen oder entfernen" @@ -711,12 +780,11 @@ msgstr "" "Hier sind noch keine Notizen. Erstelle eine, indem du auf \"Neue Notiz\" " "drückst." -#, fuzzy msgid "" "There is currently no notebook. Create one by clicking on \"New notebook\"." msgstr "" -"Momentan existieren noch keine Notizen. Erstelle eine, indem du auf den (+) " -"Knopf drückst." +"Momentan existieren noch keine Notizbücher. Erstelle eines, indem du auf den " +"(+) Knopf drückst." #, javascript-format msgid "Unsupported link or message: %s" @@ -735,17 +803,19 @@ msgid "Clear" msgstr "" msgid "OneDrive Login" -msgstr "OneDrive login" +msgstr "OneDrive Login" msgid "Import" msgstr "Importieren" -#, fuzzy msgid "Synchronisation Status" -msgstr "Synchronisationsziel" +msgstr "Synchronisations Status" + +msgid "Encryption Options" +msgstr "" msgid "Remove this tag from all the notes?" -msgstr "Diese Markierung von allen Notizen löschen?" +msgstr "Diese Markierung von allen Notizen entfernen?" msgid "Remove this search from the sidebar?" msgstr "Diese Suche von der Seitenleiste entfernen?" @@ -765,15 +835,13 @@ msgstr "Markierungen" msgid "Searches" msgstr "Suchen" -#, fuzzy msgid "Please select where the sync status should be exported to" msgstr "" -"Wähle bitte zuerst eine Notiz oder ein Notizbuch aus, das gelöscht werden " -"soll." +"Bitte wähle aus, wohin der Synchronisations Status exportiert werden soll" -#, fuzzy, javascript-format +#, javascript-format msgid "Usage: %s" -msgstr "Benutzung: %s" +msgstr "Nutzung: %s" #, javascript-format msgid "Unknown flag: %s" @@ -786,22 +854,22 @@ msgid "OneDrive" msgstr "OneDrive" msgid "OneDrive Dev (For testing only)" -msgstr "OneDrive Dev ( Nur für Tests )" +msgstr "OneDrive Dev (Nur für Tests)" #, javascript-format msgid "Unknown log level: %s" -msgstr "Unbekanntes Loglevel: %s" +msgstr "Unbekanntes Log Level: %s" #, javascript-format msgid "Unknown level ID: %s" -msgstr "Unbekannte Level-ID: %s" +msgstr "Unbekannte Level ID: %s" msgid "" "Cannot refresh token: authentication data is missing. Starting the " "synchronisation again may fix the problem." msgstr "" -"Kann Token nicht erneuern: Authentikationsdaten nicht vorhanden. Ein " -"Neustart der Synchronisation behebt das Problem vielleicht." +"Kann Token nicht erneuern: Authentifikationsdaten nicht vorhanden. Ein " +"Neustart der Synchronisation könnte das Problem beheben." msgid "" "Could not synchronize with OneDrive.\n" @@ -816,7 +884,7 @@ msgstr "" "Dieser Fehler kommt oft vor, wenn OneDrive Business benutzt wird, das leider " "nicht unterstützt wird.\n" "\n" -"Bitte benutze stattdessen einen normalen OneDrive account." +"Bitte benutze stattdessen einen normalen OneDrive Account." #, javascript-format msgid "Cannot access %s" @@ -824,27 +892,27 @@ msgstr "Kann nicht auf %s zugreifen" #, javascript-format msgid "Created local items: %d." -msgstr "" +msgstr "Lokale Objekte erstellt: %d." #, javascript-format msgid "Updated local items: %d." -msgstr "" +msgstr "Lokale Objekte aktualisiert: %d." #, javascript-format msgid "Created remote items: %d." -msgstr "" +msgstr "Remote Objekte erstellt: %d." #, javascript-format msgid "Updated remote items: %d." -msgstr "" +msgstr "Remote Objekte aktualisiert: %d." #, javascript-format msgid "Deleted local items: %d." -msgstr "" +msgstr "Lokale Objekte gelöscht: %d." #, javascript-format msgid "Deleted remote items: %d." -msgstr "" +msgstr "Remote Objekte gelöscht: %d." #, javascript-format msgid "State: \"%s\"." @@ -888,15 +956,15 @@ msgid "Cannot move note to \"%s\" notebook" msgstr "Kann Notiz nicht zu Notizbuch \"%s\" verschieben" msgid "Text editor" -msgstr "Textbearbeitungsprogramm" +msgstr "Textverarbeitungsprogramm" msgid "" "The editor that will be used to open a note. If none is provided it will try " "to auto-detect the default editor." msgstr "" -"Das Textbearbeitungsprogramm, mit dem Notizen geöffnet werden. Wenn keines " +"Das Textverarbeitungsprogramm, mit dem Notizen geöffnet werden. Wenn keines " "ausgewählt wurde, wird Joplin versuchen das standard-" -"Textbearbeitungsprogramm zu erkennen." +"Textverarbeitungsprogramm zu erkennen." msgid "Language" msgstr "Sprache" @@ -917,7 +985,7 @@ msgid "Dark" msgstr "Dunkel" msgid "Show uncompleted todos on top of the lists" -msgstr "Unvollständige To-Dos oben in der Liste anzeigen" +msgstr "Zeige unvollständige To-Dos oben in der Liste" msgid "Save geo-location with notes" msgstr "Momentanen Standort zusammen mit Notizen speichern" @@ -925,9 +993,6 @@ msgstr "Momentanen Standort zusammen mit Notizen speichern" msgid "Synchronisation interval" msgstr "Synchronisationsinterval" -msgid "Disabled" -msgstr "Deaktiviert" - #, javascript-format msgid "%d minutes" msgstr "%d Minuten" @@ -954,11 +1019,11 @@ msgid "" "`sync.2.path` to specify the target directory." msgstr "" "Das Synchronisationsziel, mit dem synchronisiert werden soll. Wenn mit dem " -"Dateisystem synchronisiert werden soll, setz den Wert zu `sync.2.path`, um " +"Dateisystem synchronisiert werden soll, setze den Wert zu `sync.2.path`, um " "den Zielpfad zu spezifizieren." msgid "Directory to synchronise with (absolute path)" -msgstr "" +msgstr "Verzeichnis zum synchronisieren (absoluter Pfad)" msgid "" "The path to synchronise with when file system synchronisation is enabled. " @@ -972,15 +1037,14 @@ msgid "Invalid option value: \"%s\". Possible values are: %s." msgstr "Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s." msgid "Items that cannot be synchronised" -msgstr "" +msgstr "Objekte können nicht synchronisiert werden" #, javascript-format msgid "\"%s\": \"%s\"" msgstr "\"%s\": \"%s\"" msgid "Sync status (synced items / total items)" -msgstr "" -"Synchronisationsstatus (synchronisierte Notizen / vorhandenen Notizen )" +msgstr "Synchronisationsstatus (synchronisierte Objekte / gesamte Objekte)" #, javascript-format msgid "%s: %d/%d" @@ -992,11 +1056,11 @@ msgstr "Insgesamt: %d/%d" #, javascript-format msgid "Conflicted: %d" -msgstr "" +msgstr "In Konflikt %d" -#, fuzzy, javascript-format +#, javascript-format msgid "To delete: %d" -msgstr "Zu löschende Notizen: %d" +msgstr "Zu löschen: %d" msgid "Folders" msgstr "Ordner" @@ -1023,9 +1087,6 @@ msgstr "Sollen diese Notizen gelöscht werden?" msgid "Log" msgstr "Log" -msgid "Status" -msgstr "Status" - msgid "Export Debug Report" msgstr "Fehlerbreicht exportieren" @@ -1033,14 +1094,14 @@ msgid "Configuration" msgstr "Konfiguration" msgid "Move to notebook..." -msgstr "Zu Notizbuch verschieben..." +msgstr "In Notizbuch verschieben..." #, javascript-format msgid "Move %d notes to notebook \"%s\"?" -msgstr "%d Notizen zu dem Notizbuch \"%s\" verschieben?" +msgstr "%d Notizen in das Notizbuch \"%s\" verschieben?" msgid "Select date" -msgstr "Datum ausswählen" +msgstr "Datum auswählen" msgid "Confirm" msgstr "Bestätigen" @@ -1075,10 +1136,10 @@ msgid "Attach any file" msgstr "Beliebige Datei anhängen" msgid "Convert to note" -msgstr "Zu einer Notiz umwandeln" +msgstr "In eine Notiz umwandeln" msgid "Convert to todo" -msgstr "Zu einem To-Do umwandeln" +msgstr "In ein To-Do umwandeln" msgid "Hide metadata" msgstr "Metadaten verstecken" @@ -1100,19 +1161,16 @@ msgid "" "menu to access your existing notebooks." msgstr "" "Drücke auf den (+) Knopf, um eine neue Notiz oder ein neues Notizbuch zu " -"erstellen." +"erstellen. Tippe auf die Seitenleiste, um auf deine existierenden " +"Notizbücher zuzugreifen." msgid "You currently have no notebook. Create one by clicking on (+) button." msgstr "" -"Du hast noch kein Notizbuch. Du kannst eines erstellen, indem du auf den (+) " -"Knopf drückst." +"Du hast noch kein Notizbuch. Erstelle eines, indem du auf den (+) Knopf " +"drückst." msgid "Welcome" -msgstr "Wilkommen" - -#, fuzzy -#~ msgid "Some items cannot be decrypted." -#~ msgstr "Kann Synchronisierer nicht initialisieren." +msgstr "Willkommen" #~ msgid "Delete notebook?" #~ msgstr "Notizbuch löschen?" diff --git a/CliClient/locales/en_GB.po b/CliClient/locales/en_GB.po index d805c6765..21fe02e7c 100644 --- a/CliClient/locales/en_GB.po +++ b/CliClient/locales/en_GB.po @@ -559,6 +559,67 @@ msgstr "" msgid "Save" msgstr "" +msgid "" +"Disabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent unencrypted to the sync target. Do you wish to " +"continue?" +msgstr "" + +msgid "" +"Enabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent encrypted to the sync target. Do not lose the " +"password as, for security purposes, this will be the *only* way to decrypt " +"the data! To enable encryption, please enter your password below." +msgstr "" + +msgid "Disable encryption" +msgstr "" + +msgid "Enable encryption" +msgstr "" + +msgid "Master Keys" +msgstr "" + +msgid "Active" +msgstr "" + +msgid "ID" +msgstr "" + +msgid "Source" +msgstr "" + +msgid "Created" +msgstr "" + +msgid "Updated" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password OK" +msgstr "" + +msgid "" +"Note: Only one master key is going to be used for encryption (the one marked " +"as \"active\"). Any of the keys might be used for decryption, depending on " +"how the notes or notebooks were originally encrypted." +msgstr "" + +msgid "Status" +msgstr "" + +msgid "Encryption is:" +msgstr "" + +msgid "Enabled" +msgstr "" + +msgid "Disabled" +msgstr "" + msgid "Back" msgstr "" @@ -603,16 +664,10 @@ msgstr "" msgid "View them now" msgstr "" -msgid "ID" +msgid "Some items cannot be decrypted." msgstr "" -msgid "Source" -msgstr "" - -msgid "Created" -msgstr "" - -msgid "Updated" +msgid "Set the password" msgstr "" msgid "Add or remove tags" @@ -659,6 +714,9 @@ msgstr "" msgid "Synchronisation Status" msgstr "" +msgid "Encryption Options" +msgstr "" + msgid "Remove this tag from all the notes?" msgstr "" @@ -825,9 +883,6 @@ msgstr "" msgid "Synchronisation interval" msgstr "" -msgid "Disabled" -msgstr "" - #, javascript-format msgid "%d minutes" msgstr "" @@ -915,9 +970,6 @@ msgstr "" msgid "Log" msgstr "" -msgid "Status" -msgstr "" - msgid "Export Debug Report" msgstr "" diff --git a/CliClient/locales/es_CR.po b/CliClient/locales/es_CR.po index c03053480..5dbeb9b76 100644 --- a/CliClient/locales/es_CR.po +++ b/CliClient/locales/es_CR.po @@ -615,6 +615,70 @@ msgstr "" msgid "Save" msgstr "" +msgid "" +"Disabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent unencrypted to the sync target. Do you wish to " +"continue?" +msgstr "" + +msgid "" +"Enabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent encrypted to the sync target. Do not lose the " +"password as, for security purposes, this will be the *only* way to decrypt " +"the data! To enable encryption, please enter your password below." +msgstr "" + +msgid "Disable encryption" +msgstr "" + +msgid "Enable encryption" +msgstr "" + +msgid "Master Keys" +msgstr "" + +msgid "Active" +msgstr "" + +msgid "ID" +msgstr "" + +msgid "Source" +msgstr "" + +#, fuzzy +msgid "Created" +msgstr "Creado: %d." + +#, fuzzy +msgid "Updated" +msgstr "Actualizado: %d." + +msgid "Password" +msgstr "" + +msgid "Password OK" +msgstr "" + +msgid "" +"Note: Only one master key is going to be used for encryption (the one marked " +"as \"active\"). Any of the keys might be used for decryption, depending on " +"how the notes or notebooks were originally encrypted." +msgstr "" + +msgid "Status" +msgstr "Estatus" + +msgid "Encryption is:" +msgstr "" + +#, fuzzy +msgid "Enabled" +msgstr "Deshabilitado" + +msgid "Disabled" +msgstr "Deshabilitado" + #, fuzzy msgid "Back" msgstr "Retroceder" @@ -662,19 +726,12 @@ msgstr "No se puede inicializar sincronizador." msgid "View them now" msgstr "" -msgid "ID" -msgstr "" - -msgid "Source" -msgstr "" - #, fuzzy -msgid "Created" -msgstr "Creado: %d." +msgid "Some items cannot be decrypted." +msgstr "No se puede inicializar sincronizador." -#, fuzzy -msgid "Updated" -msgstr "Actualizado: %d." +msgid "Set the password" +msgstr "" msgid "Add or remove tags" msgstr "Agregar o borrar etiquetas" @@ -724,6 +781,9 @@ msgstr "Importar" msgid "Synchronisation Status" msgstr "Sincronización de objetivo" +msgid "Encryption Options" +msgstr "" + msgid "Remove this tag from all the notes?" msgstr "Remover esta etiqueta de todas las notas?" @@ -907,9 +967,6 @@ msgstr "Guardar notas con geo-licalización" msgid "Synchronisation interval" msgstr "Intervalo de sincronización" -msgid "Disabled" -msgstr "Deshabilitado" - #, javascript-format msgid "%d minutes" msgstr "%d minutos" @@ -1006,9 +1063,6 @@ msgstr "Borrar estas notas?" msgid "Log" msgstr "Log" -msgid "Status" -msgstr "Estatus" - #, fuzzy msgid "Export Debug Report" msgstr "Exportar reporte depuracion" diff --git a/CliClient/locales/es_ES.po b/CliClient/locales/es_ES.po new file mode 100644 index 000000000..e8ad1b363 --- /dev/null +++ b/CliClient/locales/es_ES.po @@ -0,0 +1,1151 @@ +# Joplin translation to Spanish (Spain) +# Copyright (C) 2017 Lucas Vieites +# This file is distributed under the same license as the Joplin-CLI package. +# Lucas Vieites , 2017. +# +msgid "" +msgstr "" +"Project-Id-Version: Joplin-CLI 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Lucas Vieites\n" +"Language-Team: Spanish \n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.8.11\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Poedit-SourceCharset: UTF-8\n" + +msgid "Give focus to next pane" +msgstr "Enfocar el siguiente panel" + +msgid "Give focus to previous pane" +msgstr "Enfocar el panel anterior" + +msgid "Enter command line mode" +msgstr "Entrar en modo línea de comandos" + +msgid "Exit command line mode" +msgstr "Salir del modo línea de comandos" + +msgid "Edit the selected note" +msgstr "Editar la nota seleccionada" + +msgid "Cancel the current command." +msgstr "Cancelar el comando actual." + +msgid "Exit the application." +msgstr "Salir de la aplicación." + +msgid "Delete the currently selected note or notebook." +msgstr "Eliminar la nota o libreta seleccionada." + +msgid "To delete a tag, untag the associated notes." +msgstr "Desmarque las notas asociadas para eliminar una etiqueta." + +msgid "Please select the note or notebook to be deleted first." +msgstr "Seleccione primero la nota o libreta que desea eliminar." + +msgid "Set a to-do as completed / not completed" +msgstr "Marca una tarea como completada/no completada" + +msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible." +msgstr "in[t]ercambia la [c]onsola entre maximizada/minimizada/oculta/visible." + +msgid "Search" +msgstr "Buscar" + +msgid "[t]oggle note [m]etadata." +msgstr "in[t]ercambia los [m]etadatos de una nota." + +msgid "[M]ake a new [n]ote" +msgstr "[C]rear una [n]ota nueva" + +msgid "[M]ake a new [t]odo" +msgstr "[C]rear una [t]area nueva" + +msgid "[M]ake a new note[b]ook" +msgstr "[C]rear una li[b]reta nueva" + +msgid "Copy ([Y]ank) the [n]ote to a notebook." +msgstr "Copiar ([Y]ank) la [n]ota a una libreta." + +msgid "Move the note to a notebook." +msgstr "Mover la nota a una libreta." + +msgid "Press Ctrl+D or type \"exit\" to exit the application" +msgstr "Pulse Ctrl+D o escriba «salir» para salir de la aplicación" + +#, javascript-format +msgid "More than one item match \"%s\". Please narrow down your query." +msgstr "" +"Hay más de un elemento que coincide con «%s», intente mejorar su consulta." + +msgid "No notebook selected." +msgstr "No se ha seleccionado ninguna libreta." + +msgid "No notebook has been specified." +msgstr "Ninguna libre fue especificada" + +msgid "Y" +msgstr "Y" + +msgid "n" +msgstr "n" + +msgid "N" +msgstr "N" + +msgid "y" +msgstr "y" + +msgid "Cancelling background synchronisation... Please wait." +msgstr "Cancelando sincronización de segundo plano... Por favor espere." + +#, javascript-format +msgid "No such command: %s" +msgstr "El comando no existe: %s" + +#, javascript-format +msgid "The command \"%s\" is only available in GUI mode" +msgstr "El comando «%s» solamente está disponible en modo GUI" + +#, javascript-format +msgid "Missing required argument: %s" +msgstr "Falta un argumento requerido: %s" + +#, javascript-format +msgid "%s: %s" +msgstr "%s: %s" + +msgid "Your choice: " +msgstr "Tu elección: " + +#, javascript-format +msgid "Invalid answer: %s" +msgstr "Respuesta inválida: %s" + +msgid "Attaches the given file to the note." +msgstr "Adjuntar archivo a la nota." + +#, javascript-format +msgid "Cannot find \"%s\"." +msgstr "No se encuentra \"%s\"." + +msgid "Displays the given note." +msgstr "Mostrar la nota dada." + +msgid "Displays the complete information about note." +msgstr "Mostrar la información completa acerca de la nota." + +msgid "" +"Gets or sets a config value. If [value] is not provided, it will show the " +"value of [name]. If neither [name] nor [value] is provided, it will list the " +"current configuration." +msgstr "" +"Obtener o configurar un valor. Si no se provee el [valor], se mostrará el " +"valor de [nombre]. Si no se provee [nombre] ni [valor], se listara la " +"configuración actual." + +msgid "Also displays unset and hidden config variables." +msgstr "También muestra variables ocultas o no configuradas." + +#, javascript-format +msgid "%s = %s (%s)" +msgstr "%s = %s (%s)" + +#, javascript-format +msgid "%s = %s" +msgstr "%s = %s" + +msgid "" +"Duplicates the notes matching to [notebook]. If no notebook is " +"specified the note is duplicated in the current notebook." +msgstr "" +"Duplica las notas que coincidan con en la libreta. Si no se " +"especifica una libreta la nota se duplica en la libreta actual." + +msgid "Marks a to-do as done." +msgstr "Marca una tarea como hecha." + +#, javascript-format +msgid "Note is not a to-do: \"%s\"" +msgstr "Una nota no es una tarea: \"%s\"" + +msgid "Edit note." +msgstr "Editar una nota." + +msgid "" +"No text editor is defined. Please set it using `config editor `" +msgstr "" +"No hay editor de texto definido. Por favor configure uno usando `config " +"editor `" + +msgid "No active notebook." +msgstr "No hay libreta activa." + +#, javascript-format +msgid "Note does not exist: \"%s\". Create it?" +msgstr "La nota no existe: \"%s\". Crearla?" + +msgid "Starting to edit note. Close the editor to get back to the prompt." +msgstr "Iniciando a editar una nota. Cierra el editor para regresar al prompt." + +msgid "Note has been saved." +msgstr "La nota a sido guardada." + +msgid "Exits the application." +msgstr "Sale de la aplicación." + +msgid "" +"Exports Joplin data to the given directory. By default, it will export the " +"complete database including notebooks, notes, tags and resources." +msgstr "" +"Exportar datos de Joplin al directorio indicado. Por defecto, se exportará " +"la base de datos completa incluyendo libretas, notas, etiquetas y recursos." + +msgid "Exports only the given note." +msgstr "Exportar unicamente la nota indicada." + +msgid "Exports only the given notebook." +msgstr "Exportar unicamente la libreta indicada." + +msgid "Displays a geolocation URL for the note." +msgstr "Mostrar geolocalización de la URL para la nota." + +msgid "Displays usage information." +msgstr "Muestra información de uso." + +msgid "Shortcuts are not available in CLI mode." +msgstr "Atajos no disponibles en modo CLI." + +msgid "" +"Type `help [command]` for more information about a command; or type `help " +"all` for the complete usage information." +msgstr "" +"Escriba `help [command]` para obtener más información sobre el comando, o " +"escriba `help all` para obtener toda la información acerca del uso del " +"programa." + +msgid "The possible commands are:" +msgstr "Los posibles comandos son:" + +msgid "" +"In any command, a note or notebook can be refered to by title or ID, or " +"using the shortcuts `$n` or `$b` for, respectively, the currently selected " +"note or notebook. `$c` can be used to refer to the currently selected item." +msgstr "" +"Con cualquier comando, una nota o libreta puede ser referida por titulo o " +"ID, o utilizando atajos `$n` o `$b`, respectivamente, para la nota o libreta " +"seleccionada se puede usar `$c` para hacer referencia al artículo " +"seleccionado." + +msgid "To move from one pane to another, press Tab or Shift+Tab." +msgstr "Para mover desde un panel a otro, presiona Tab o Shift+Tab." + +msgid "" +"Use the arrows and page up/down to scroll the lists and text areas " +"(including this console)." +msgstr "" +"Para desplazar en las listas y areas de texto ( incluyendo la consola ) " +"utilice las flechas y re pág/av pág." + +msgid "To maximise/minimise the console, press \"TC\"." +msgstr "Para maximizar/minimizar la consola, presiona \"TC\"." + +msgid "To enter command line mode, press \":\"" +msgstr "Para entrar a modo linea de comando, presiona \":\"" + +msgid "To exit command line mode, press ESCAPE" +msgstr "Para salir de modo linea de comando, presiona ESCAPE" + +msgid "" +"For the complete list of available keyboard shortcuts, type `help shortcuts`" +msgstr "" +"Para una lista completa de los atajos de teclado disponibles, escribe `help " +"shortcuts`" + +msgid "Imports an Evernote notebook file (.enex file)." +msgstr "Importar una libreta de Evernote (archivo .enex)." + +msgid "Do not ask for confirmation." +msgstr "No preguntar por confirmación." + +#, javascript-format +msgid "File \"%s\" will be imported into existing notebook \"%s\". Continue?" +msgstr "" +"El archivo \"%s\" será importado dentro de la libreta existente \"%s\". " +"Continuar?" + +#, javascript-format +msgid "" +"New notebook \"%s\" will be created and file \"%s\" will be imported into " +"it. Continue?" +msgstr "" +"Nueva libreta \"%s\" será creada y el archivo \"%s\" será importado dentro " +"de ella. Continuar?" + +#, javascript-format +msgid "Found: %d." +msgstr "Encontrado: %d." + +#, javascript-format +msgid "Created: %d." +msgstr "Creado: %d." + +#, javascript-format +msgid "Updated: %d." +msgstr "Actualizado: %d." + +#, javascript-format +msgid "Skipped: %d." +msgstr "Omitido: %d." + +#, javascript-format +msgid "Resources: %d." +msgstr "Recursos: %d." + +#, javascript-format +msgid "Tagged: %d." +msgstr "Etiquetado: %d." + +msgid "Importing notes..." +msgstr "Importando notas..." + +#, javascript-format +msgid "The notes have been imported: %s" +msgstr "Las notas han sido importadas: %s" + +msgid "" +"Displays the notes in the current notebook. Use `ls /` to display the list " +"of notebooks." +msgstr "" +"Muestra las notas en la libreta actual. Usa `ls /` para mostrar la lista de " +"libretas." + +msgid "Displays only the first top notes." +msgstr "Muestra las primeras notas." + +msgid "Sorts the item by (eg. title, updated_time, created_time)." +msgstr "" +"Ordena los artículos por campo ( ej. título, fecha de actualización, fecha " +"de creación)." + +msgid "Reverses the sorting order." +msgstr "Invierte el orden." + +msgid "" +"Displays only the items of the specific type(s). Can be `n` for notes, `t` " +"for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the " +"to-dos, while `-ttd` would display notes and to-dos." +msgstr "" +"Muestra unicamente los artículos de los tipos especificados. Pueden ser `n` " +"para notas, `t` para tareas, o `nt` para libretas y tareas (ej. `-tt` " +"mostrará unicamente las tareas, mientras `-ttd` mostrará notas y tareas)." + +msgid "Either \"text\" or \"json\"" +msgstr "Puede ser \"text\" o \"json\"" + +msgid "" +"Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, " +"TODO_CHECKED (for to-dos), TITLE" +msgstr "" +"Usar formato largo de lista. El formato es ID, NOTE_COUNT ( para libretas), " +"DATE,TODO_CHECKED ( para tareas), TITLE" + +msgid "Please select a notebook first." +msgstr "Por favor selecciona la libreta." + +msgid "Creates a new notebook." +msgstr "Crea una nueva libreta." + +msgid "Creates a new note." +msgstr "Crea una nueva nota." + +msgid "Notes can only be created within a notebook." +msgstr "Notas solamente pueden ser creadas dentro de una libreta." + +msgid "Creates a new to-do." +msgstr "Crea una nueva lista de tareas." + +msgid "Moves the notes matching to [notebook]." +msgstr "Mueve las notas que coincidan con para la [libreta]." + +msgid "Renames the given (note or notebook) to ." +msgstr "Renombre el artículo dado (nota o libreta) a ." + +msgid "Deletes the given notebook." +msgstr "Elimina la libreta dada." + +msgid "Deletes the notebook without asking for confirmation." +msgstr "Elimina una libreta sin pedir confirmación." + +msgid "Delete notebook? All notes within this notebook will also be deleted." +msgstr "" +"¿Desea eliminar la libreta? Todas las notas dentro de esta libreta también " +"serán eliminadas." + +msgid "Deletes the notes matching ." +msgstr "Elimina las notas que coinciden con ." + +msgid "Deletes the notes without asking for confirmation." +msgstr "Elimina las notas sin pedir confirmación." + +#, javascript-format +msgid "%d notes match this pattern. Delete them?" +msgstr "%d notas coinciden con el patron. Eliminarlas?" + +msgid "Delete note?" +msgstr "Eliminar nota?" + +msgid "Searches for the given in all the notes." +msgstr "Buscar el patron en todas las notas." + +#, javascript-format +msgid "" +"Sets the property of the given to the given [value]. Possible " +"properties are:\n" +"\n" +"%s" +msgstr "" +"Asigna el valor [value] a la propiedad de la nota indicada . " +"Propiedades disponibles:\n" +"\n" +"%s" + +msgid "Displays summary about the notes and notebooks." +msgstr "Muestra un resumen acerca de las notas y las libretas." + +msgid "Synchronises with remote storage." +msgstr "Sincronizar con almacenamiento remoto." + +msgid "Sync to provided target (defaults to sync.target config value)" +msgstr "" +"Sincronizar con objetivo proveído ( por defecto al valor de configuración " +"sync.target)" + +msgid "Synchronisation is already in progress." +msgstr "Sincronzación en progreso." + +#, javascript-format +msgid "" +"Lock file is already being hold. If you know that no synchronisation is " +"taking place, you may delete the lock file at \"%s\" and resume the " +"operation." +msgstr "" +"Ya hay un archivo de bloqueo. Si está seguro de que no hay una " +"sincronización en curso puede eliminar el archivo de bloqueo «%s» y reanudar " +"la operación." + +msgid "" +"Authentication was not completed (did not receive an authentication token)." +msgstr "Autenticación no completada (no se recibió token de autenticación)." + +#, javascript-format +msgid "Synchronisation target: %s (%s)" +msgstr "Objetivo de sincronización: %s (%s)" + +msgid "Cannot initialize synchroniser." +msgstr "No se puede inicializar sincronizador." + +msgid "Starting synchronisation..." +msgstr "Iniciando sincronización..." + +msgid "Cancelling... Please wait." +msgstr "Cancelando... Por favor espere." + +msgid "" +" can be \"add\", \"remove\" or \"list\" to assign or remove " +"[tag] from [note], or to list the notes associated with [tag]. The command " +"`tag list` can be used to list all the tags." +msgstr "" +" puede ser \"add\", \"remove\" o \"list\" para asignar o " +"eliminar [tag] de [note], o para listar las notas asociadas con [tag]. El " +"comando `tag list` puede ser usado para listar todas las etiquetas." + +#, javascript-format +msgid "Invalid command: \"%s\"" +msgstr "Comando inválido: \"%s\"" + +msgid "" +" can either be \"toggle\" or \"clear\". Use \"toggle\" to " +"toggle the given to-do between completed and uncompleted state (If the " +"target is a regular note it will be converted to a to-do). Use \"clear\" to " +"convert the to-do back to a regular note." +msgstr "" +" puede ser \"toggle\" o \"clear\". Usa \"toggle\" para cambiar " +"la tarea dada entre estado completado y sin completar. ( Si el objetivo es " +"una nota regular se convertirá en una tarea). Usa \"clear\" para convertir " +"la tarea a una nota regular. " + +msgid "Marks a to-do as non-completed." +msgstr "Marcar una tarea como no completada." + +msgid "" +"Switches to [notebook] - all further operations will happen within this " +"notebook." +msgstr "" +"Cambia una [libreta] - todas las demás operaciones se realizan en ésta " +"libreta." + +msgid "Displays version information" +msgstr "Muestra información de la versión" + +#, javascript-format +msgid "%s %s (%s)" +msgstr "%s %s (%s)" + +msgid "Enum" +msgstr "Enumerar" + +#, javascript-format +msgid "Type: %s." +msgstr "Tipo: %s." + +#, javascript-format +msgid "Possible values: %s." +msgstr "Posibles valores: %s." + +#, javascript-format +msgid "Default: %s" +msgstr "Por defecto: %s" + +msgid "Possible keys/values:" +msgstr "Teclas/valores posbiles:" + +msgid "Fatal error:" +msgstr "Error fatal:" + +msgid "" +"The application has been authorised - you may now close this browser tab." +msgstr "" +"La aplicación ha sido autorizada - ahora puedes cerrar esta pestaña de tu " +"navegador." + +msgid "The application has been successfully authorised." +msgstr "La aplicacion ha sido autorizada exitosamente." + +msgid "" +"Please open the following URL in your browser to authenticate the " +"application. The application will create a directory in \"Apps/Joplin\" and " +"will only read and write files in this directory. It will have no access to " +"any files outside this directory nor to any other personal data. No data " +"will be shared with any third party." +msgstr "" +"Abra la siguiente URL en su navegador para autenticar la aplicación. La " +"aplicación creará un directorio en «Apps/Joplin» y solo leerá y escribirá " +"archivos en este directorio. No tendrá acceso a ningún archivo fuera de este " +"directorio ni a ningún otro archivo personal. No se compartirá información " +"con terceros." + +msgid "Search:" +msgstr "Buscar:" + +msgid "" +"Welcome to Joplin!\n" +"\n" +"Type `:help shortcuts` for the list of keyboard shortcuts, or just `:help` " +"for usage information.\n" +"\n" +"For example, to create a notebook press `mb`; to create a note press `mn`." +msgstr "" +"Bienvenido a Joplin.\n" +"\n" +"Escriba «:help shortcuts» para obtener una lista con los atajos de teclado, " +"o simplemente «:help» para información general.\n" +"\n" +"Por ejemplo, para crear una libreta escriba «mb», para crear una nota " +"escriba «mn»." + +msgid "File" +msgstr "Archivo" + +msgid "New note" +msgstr "Nota nueva" + +msgid "New to-do" +msgstr "Lista de tareas nueva" + +msgid "New notebook" +msgstr "Libreta nueva" + +msgid "Import Evernote notes" +msgstr "Importar notas de Evernote" + +msgid "Evernote Export Files" +msgstr "Archivos exportados de Evernote" + +msgid "Quit" +msgstr "Salir" + +msgid "Edit" +msgstr "Editar" + +msgid "Copy" +msgstr "Copiar" + +msgid "Cut" +msgstr "Cortar" + +msgid "Paste" +msgstr "Pegar" + +msgid "Search in all the notes" +msgstr "Buscar en todas las notas" + +msgid "Tools" +msgstr "Herramientas" + +msgid "Synchronisation status" +msgstr "Estado de la sincronización" + +msgid "Options" +msgstr "Opciones" + +msgid "Help" +msgstr "Ayuda" + +msgid "Website and documentation" +msgstr "Sitio web y documentación" + +msgid "About Joplin" +msgstr "Acerca de Joplin" + +#, javascript-format +msgid "%s %s (%s, %s)" +msgstr "%s %s (%s, %s)" + +msgid "OK" +msgstr "OK" + +msgid "Cancel" +msgstr "Cancelar" + +#, javascript-format +msgid "Notes and settings are stored in: %s" +msgstr "Las notas y los ajustes se guardan en: %s" + +msgid "Save" +msgstr "Guardar" + +msgid "" +"Disabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent unencrypted to the sync target. Do you wish to " +"continue?" +msgstr "" + +msgid "" +"Enabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent encrypted to the sync target. Do not lose the " +"password as, for security purposes, this will be the *only* way to decrypt " +"the data! To enable encryption, please enter your password below." +msgstr "" + +msgid "Disable encryption" +msgstr "" + +msgid "Enable encryption" +msgstr "" + +msgid "Master Keys" +msgstr "" + +msgid "Active" +msgstr "" + +msgid "ID" +msgstr "ID" + +msgid "Source" +msgstr "Origen" + +msgid "Created" +msgstr "Creado" + +msgid "Updated" +msgstr "Actualizado" + +msgid "Password" +msgstr "" + +msgid "Password OK" +msgstr "" + +msgid "" +"Note: Only one master key is going to be used for encryption (the one marked " +"as \"active\"). Any of the keys might be used for decryption, depending on " +"how the notes or notebooks were originally encrypted." +msgstr "" + +msgid "Status" +msgstr "Estado" + +msgid "Encryption is:" +msgstr "" + +#, fuzzy +msgid "Enabled" +msgstr "Deshabilitado" + +msgid "Disabled" +msgstr "Deshabilitado" + +msgid "Back" +msgstr "Atrás" + +#, javascript-format +msgid "" +"New notebook \"%s\" will be created and file \"%s\" will be imported into it" +msgstr "Se creará la nueva libreta «%s» y se importará en ella el archivo «%s»" + +msgid "Please create a notebook first." +msgstr "Cree primero una libreta." + +msgid "Note title:" +msgstr "Título de la nota:" + +msgid "Please create a notebook first" +msgstr "Por favor crea una libreta primero" + +msgid "To-do title:" +msgstr "Títuto de lista de tareas:" + +msgid "Notebook title:" +msgstr "Título de libreta:" + +msgid "Add or remove tags:" +msgstr "Agregar o borrar etiquetas: " + +msgid "Separate each tag by a comma." +msgstr "Separar cada etiqueta por una coma." + +msgid "Rename notebook:" +msgstr "Renombrar libreta:" + +msgid "Set alarm:" +msgstr "Ajustar alarma:" + +msgid "Layout" +msgstr "Diseño" + +msgid "Some items cannot be synchronised." +msgstr "No se han podido sincronizar algunos de los elementos." + +msgid "View them now" +msgstr "Verlos ahora" + +#, fuzzy +msgid "Some items cannot be decrypted." +msgstr "No se han podido sincronizar algunos de los elementos." + +msgid "Set the password" +msgstr "" + +msgid "Add or remove tags" +msgstr "Añadir o borrar etiquetas" + +msgid "Switch between note and to-do type" +msgstr "Cambiar entre nota y lista de tareas" + +msgid "Delete" +msgstr "Eliminar" + +msgid "Delete notes?" +msgstr "¿Desea eliminar notas?" + +msgid "No notes in here. Create one by clicking on \"New note\"." +msgstr "No hay ninguna nota. Cree una pulsando «Nota nueva»." + +msgid "" +"There is currently no notebook. Create one by clicking on \"New notebook\"." +msgstr "No hay ninguna libreta. Cree una pulsando en «Libreta nueva»." + +#, javascript-format +msgid "Unsupported link or message: %s" +msgstr "Enlace o mensaje no soportado: %s" + +msgid "Attach file" +msgstr "Adjuntar archivo" + +msgid "Set alarm" +msgstr "Fijar alarma" + +msgid "Refresh" +msgstr "Refrescar" + +msgid "Clear" +msgstr "Limpiar" + +msgid "OneDrive Login" +msgstr "Inicio de sesión de OneDrive" + +msgid "Import" +msgstr "Importar" + +msgid "Synchronisation Status" +msgstr "Estado de la sincronización" + +msgid "Encryption Options" +msgstr "" + +msgid "Remove this tag from all the notes?" +msgstr "¿Desea eliminar esta etiqueta de todas las notas?" + +msgid "Remove this search from the sidebar?" +msgstr "¿Desea eliminar esta búsqueda de la barra lateral?" + +msgid "Rename" +msgstr "Renombrar" + +msgid "Synchronise" +msgstr "Sincronizar" + +msgid "Notebooks" +msgstr "Libretas" + +msgid "Tags" +msgstr "Etiquetas" + +msgid "Searches" +msgstr "Búsquedas" + +msgid "Please select where the sync status should be exported to" +msgstr "Seleccione a dónde se debería exportar el estado de sincronización" + +#, javascript-format +msgid "Usage: %s" +msgstr "Uso: %s" + +#, javascript-format +msgid "Unknown flag: %s" +msgstr "Etiqueta desconocida: %s" + +msgid "File system" +msgstr "Sistema de archivos" + +msgid "OneDrive" +msgstr "OneDrive" + +msgid "OneDrive Dev (For testing only)" +msgstr "OneDrive Dev (Solo para pruebas)" + +#, javascript-format +msgid "Unknown log level: %s" +msgstr "Nivel de log desconocido: %s" + +#, javascript-format +msgid "Unknown level ID: %s" +msgstr "ID de nivel desconocido: %s" + +msgid "" +"Cannot refresh token: authentication data is missing. Starting the " +"synchronisation again may fix the problem." +msgstr "" +"No se ha podido actualizar token: faltan datos de autenticación. Reiniciar " +"la sincronización podría solucionar el problema." + +msgid "" +"Could not synchronize with OneDrive.\n" +"\n" +"This error often happens when using OneDrive for Business, which " +"unfortunately cannot be supported.\n" +"\n" +"Please consider using a regular OneDrive account." +msgstr "" +"No se ha podido sincronizar con OneDrive.\n" +"\n" +"Este error suele ocurrir al utilizar OneDrive for Business. Este producto no " +"está soportado.\n" +"\n" +"Podría considerar utilizar una cuenta Personal de OneDrive." + +#, javascript-format +msgid "Cannot access %s" +msgstr "No se ha podido acceder a %s" + +#, javascript-format +msgid "Created local items: %d." +msgstr "Elementos locales creados: %d." + +#, javascript-format +msgid "Updated local items: %d." +msgstr "Elementos locales actualizados: %d." + +#, javascript-format +msgid "Created remote items: %d." +msgstr "Elementos remotos creados: %d." + +#, javascript-format +msgid "Updated remote items: %d." +msgstr "Elementos remotos actualizados: %d." + +#, javascript-format +msgid "Deleted local items: %d." +msgstr "Elementos locales borrados: %d." + +#, javascript-format +msgid "Deleted remote items: %d." +msgstr "Elementos remotos borrados: %d." + +#, javascript-format +msgid "State: \"%s\"." +msgstr "Estado: «%s»." + +msgid "Cancelling..." +msgstr "Cancelando..." + +#, javascript-format +msgid "Completed: %s" +msgstr "Completado: %s" + +#, javascript-format +msgid "Synchronisation is already in progress. State: %s" +msgstr "La sincronización ya está en progreso. Estado: %s" + +msgid "Conflicts" +msgstr "Conflictos" + +#, javascript-format +msgid "A notebook with this title already exists: \"%s\"" +msgstr "Ya existe una libreta con este nombre: «%s»" + +#, javascript-format +msgid "Notebooks cannot be named \"%s\", which is a reserved title." +msgstr "" +"No se puede usar el nombre «%s» para una libreta; es un título reservado." + +msgid "Untitled" +msgstr "Sin título" + +msgid "This note does not have geolocation information." +msgstr "Esta nota no tiene informacion de geolocalización." + +#, javascript-format +msgid "Cannot copy note to \"%s\" notebook" +msgstr "No se ha podido copiar la nota a la libreta «%s»" + +#, javascript-format +msgid "Cannot move note to \"%s\" notebook" +msgstr "No se ha podido mover la nota a la libreta «%s»" + +msgid "Text editor" +msgstr "Editor de texto" + +msgid "" +"The editor that will be used to open a note. If none is provided it will try " +"to auto-detect the default editor." +msgstr "" +"El editor que se usará para abrir una nota. Se intentará auto-detectar el " +"editor predeterminado si no se proporciona ninguno." + +msgid "Language" +msgstr "Idioma" + +msgid "Date format" +msgstr "Formato de fecha" + +msgid "Time format" +msgstr "Formato de hora" + +msgid "Theme" +msgstr "Tema" + +msgid "Light" +msgstr "Claro" + +msgid "Dark" +msgstr "Oscuro" + +msgid "Show uncompleted todos on top of the lists" +msgstr "Mostrar tareas incompletas al inicio de las listas" + +msgid "Save geo-location with notes" +msgstr "Guardar geolocalización en las notas" + +msgid "Synchronisation interval" +msgstr "Intervalo de sincronización" + +#, javascript-format +msgid "%d minutes" +msgstr "%d minutos" + +#, javascript-format +msgid "%d hour" +msgstr "%d hora" + +#, javascript-format +msgid "%d hours" +msgstr "%d horas" + +msgid "Automatically update the application" +msgstr "Actualizar la aplicación automáticamente" + +msgid "Show advanced options" +msgstr "Mostrar opciones avanzadas" + +msgid "Synchronisation target" +msgstr "Destino de sincronización" + +msgid "" +"The target to synchonise to. If synchronising with the file system, set " +"`sync.2.path` to specify the target directory." +msgstr "" +"El destino de la sincronización. Si se sincroniza con el sistema de " +"archivos, indique el directorio destino en «sync.2.path»." + +msgid "Directory to synchronise with (absolute path)" +msgstr "Directorio con el que sincronizarse (ruta completa)" + +msgid "" +"The path to synchronise with when file system synchronisation is enabled. " +"See `sync.target`." +msgstr "" +"La ruta a la que sincronizar cuando se activa la sincronización con sistema " +"de archivos. Vea «sync.target»." + +#, javascript-format +msgid "Invalid option value: \"%s\". Possible values are: %s." +msgstr "Opción inválida: «%s». Los valores posibles son: %s." + +msgid "Items that cannot be synchronised" +msgstr "Elementos que no se pueden sincronizar" + +#, javascript-format +msgid "\"%s\": \"%s\"" +msgstr "«%s»: «%s»" + +msgid "Sync status (synced items / total items)" +msgstr "Estado de sincronización (elementos sincronizados/elementos totales)" + +#, javascript-format +msgid "%s: %d/%d" +msgstr "%s: %d/%d" + +#, javascript-format +msgid "Total: %d/%d" +msgstr "Total: %d/%d" + +#, javascript-format +msgid "Conflicted: %d" +msgstr "Conflictos: %d" + +#, javascript-format +msgid "To delete: %d" +msgstr "Borrar: %d" + +msgid "Folders" +msgstr "Directorios" + +#, javascript-format +msgid "%s: %d notes" +msgstr "%s: %d notas" + +msgid "Coming alarms" +msgstr "Alarmas inminentes" + +#, javascript-format +msgid "On %s: %s" +msgstr "En %s: %s" + +msgid "There are currently no notes. Create one by clicking on the (+) button." +msgstr "No hay notas. Cree una pulsando en el botón (+)." + +msgid "Delete these notes?" +msgstr "¿Desea borrar estas notas?" + +msgid "Log" +msgstr "Log" + +msgid "Export Debug Report" +msgstr "Exportar informe de depuración" + +msgid "Configuration" +msgstr "Configuración" + +msgid "Move to notebook..." +msgstr "Mover a la libreta..." + +#, javascript-format +msgid "Move %d notes to notebook \"%s\"?" +msgstr "¿Desea mover %d notas a libreta «%s»?" + +msgid "Select date" +msgstr "Seleccione fecha" + +msgid "Confirm" +msgstr "Confirmar" + +msgid "Cancel synchronisation" +msgstr "Cancelar sincronización" + +#, javascript-format +msgid "The notebook could not be saved: %s" +msgstr "No se ha podido guardar esta libreta: %s" + +msgid "Edit notebook" +msgstr "Editar libreta" + +msgid "This note has been modified:" +msgstr "Esta nota ha sido modificada:" + +msgid "Save changes" +msgstr "Guardar cambios" + +msgid "Discard changes" +msgstr "Descartar cambios" + +#, javascript-format +msgid "Unsupported image type: %s" +msgstr "Tipo de imagen no soportado: %s" + +msgid "Attach photo" +msgstr "Adjuntar foto" + +msgid "Attach any file" +msgstr "Adjuntar cualquier archivo" + +msgid "Convert to note" +msgstr "Convertir a nota" + +msgid "Convert to todo" +msgstr "Convertir a lista de tareas" + +msgid "Hide metadata" +msgstr "Ocultar metadatos" + +msgid "Show metadata" +msgstr "Mostrar metadatos" + +msgid "View on map" +msgstr "Ver en un mapa" + +msgid "Delete notebook" +msgstr "Borrar libreta" + +msgid "Login with OneDrive" +msgstr "Acceder con OneDrive" + +msgid "" +"Click on the (+) button to create a new note or notebook. Click on the side " +"menu to access your existing notebooks." +msgstr "" +"Pulse en el botón (+) para crear una nueva nota o libreta. Pulse en el menú " +"lateral para acceder a las libretas existentes." + +msgid "You currently have no notebook. Create one by clicking on (+) button." +msgstr "" +"No hay ninguna libreta. Cree una nueva libreta pulsando en el botón (+)." + +msgid "Welcome" +msgstr "Bienvenido" + +#~ msgid "Delete notebook?" +#~ msgstr "Eliminar libreta?" + +#~ msgid "Delete notebook \"%s\"?" +#~ msgstr "Elimina una libreta \"%s\"?" + +#, fuzzy +#~ msgid "File system synchronisation target directory" +#~ msgstr "Sincronización de sistema de archivos en directorio objetivo" diff --git a/CliClient/locales/fr_FR.po b/CliClient/locales/fr_FR.po index 5a7cb8eca..fce1202e0 100644 --- a/CliClient/locales/fr_FR.po +++ b/CliClient/locales/fr_FR.po @@ -611,6 +611,70 @@ msgstr "" msgid "Save" msgstr "" +msgid "" +"Disabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent unencrypted to the sync target. Do you wish to " +"continue?" +msgstr "" + +msgid "" +"Enabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent encrypted to the sync target. Do not lose the " +"password as, for security purposes, this will be the *only* way to decrypt " +"the data! To enable encryption, please enter your password below." +msgstr "" + +msgid "Disable encryption" +msgstr "" + +msgid "Enable encryption" +msgstr "" + +msgid "Master Keys" +msgstr "" + +msgid "Active" +msgstr "" + +msgid "ID" +msgstr "" + +msgid "Source" +msgstr "" + +#, fuzzy +msgid "Created" +msgstr "Créés : %d." + +#, fuzzy +msgid "Updated" +msgstr "Mis à jour : %d." + +msgid "Password" +msgstr "" + +msgid "Password OK" +msgstr "" + +msgid "" +"Note: Only one master key is going to be used for encryption (the one marked " +"as \"active\"). Any of the keys might be used for decryption, depending on " +"how the notes or notebooks were originally encrypted." +msgstr "" + +msgid "Status" +msgstr "État" + +msgid "Encryption is:" +msgstr "" + +#, fuzzy +msgid "Enabled" +msgstr "Désactivé" + +msgid "Disabled" +msgstr "Désactivé" + msgid "Back" msgstr "Retour" @@ -659,19 +723,12 @@ msgstr "Impossible d'initialiser la synchronisation." msgid "View them now" msgstr "" -msgid "ID" -msgstr "" - -msgid "Source" -msgstr "" - #, fuzzy -msgid "Created" -msgstr "Créés : %d." +msgid "Some items cannot be decrypted." +msgstr "Impossible d'initialiser la synchronisation." -#, fuzzy -msgid "Updated" -msgstr "Mis à jour : %d." +msgid "Set the password" +msgstr "" msgid "Add or remove tags" msgstr "Gérer les étiquettes" @@ -723,6 +780,9 @@ msgstr "Importer" msgid "Synchronisation Status" msgstr "Cible de la synchronisation" +msgid "Encryption Options" +msgstr "" + msgid "Remove this tag from all the notes?" msgstr "Enlever cette étiquette de toutes les notes ?" @@ -894,9 +954,6 @@ msgstr "Enregistrer l'emplacement avec les notes" msgid "Synchronisation interval" msgstr "Intervalle de synchronisation" -msgid "Disabled" -msgstr "Désactivé" - #, javascript-format msgid "%d minutes" msgstr "%d minutes" @@ -990,9 +1047,6 @@ msgstr "Supprimer ces notes ?" msgid "Log" msgstr "Journal" -msgid "Status" -msgstr "État" - msgid "Export Debug Report" msgstr "Exporter rapport de débogage" @@ -1077,10 +1131,6 @@ msgstr "" msgid "Welcome" msgstr "Bienvenue" -#, fuzzy -#~ msgid "Some items cannot be decrypted." -#~ msgstr "Impossible d'initialiser la synchronisation." - #~ msgid "Delete notebook?" #~ msgstr "Supprimer le carnet ?" diff --git a/CliClient/locales/hr_HR.po b/CliClient/locales/hr_HR.po new file mode 100644 index 000000000..7cb54ca14 --- /dev/null +++ b/CliClient/locales/hr_HR.po @@ -0,0 +1,1147 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR Laurent Cozic +# This file is distributed under the same license as the Joplin-CLI package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Joplin-CLI 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Hrvoje Mandić \n" +"Language-Team: \n" +"Language: hr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.0.4\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +msgid "Give focus to next pane" +msgstr "Fokusiraj sljedeće okno" + +msgid "Give focus to previous pane" +msgstr "Fokusiraj prethodno okno" + +msgid "Enter command line mode" +msgstr "Otvori naredbeni redak" + +msgid "Exit command line mode" +msgstr "Napusti naredbeni redak" + +msgid "Edit the selected note" +msgstr "Uredi odabranu bilješku" + +msgid "Cancel the current command." +msgstr "Prekini trenutnu naredbu." + +msgid "Exit the application." +msgstr "Izađi iz aplikacije." + +msgid "Delete the currently selected note or notebook." +msgstr "Obriši odabranu bilješku ili bilježnicu." + +msgid "To delete a tag, untag the associated notes." +msgstr "Da bi mogao obrisati oznaku, skini oznaku s povezanih bilješki." + +msgid "Please select the note or notebook to be deleted first." +msgstr "Odaberi bilješku ili bilježnicu za brisanje." + +msgid "Set a to-do as completed / not completed" +msgstr "Postavi zadatak kao završen/nezavršen" + +#, fuzzy +msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible." +msgstr "[t]oggle [c]onsole between maximized/minimized/hidden/visible." + +msgid "Search" +msgstr "Traži" + +#, fuzzy +msgid "[t]oggle note [m]etadata." +msgstr "[t]oggle note [m]etadata." + +#, fuzzy +msgid "[M]ake a new [n]ote" +msgstr "[M]ake a new [n]ote" + +#, fuzzy +msgid "[M]ake a new [t]odo" +msgstr "[M]ake a new [t]odo" + +#, fuzzy +msgid "[M]ake a new note[b]ook" +msgstr "[M]ake a new note[b]ook" + +#, fuzzy +msgid "Copy ([Y]ank) the [n]ote to a notebook." +msgstr "Copy ([Y]ank) the [n]ote to a notebook." + +msgid "Move the note to a notebook." +msgstr "Premjesti bilješku u bilježnicu." + +msgid "Press Ctrl+D or type \"exit\" to exit the application" +msgstr "Pritisni Ctrl+D ili napiši \"exit\" za izlazak iz aplikacije" + +#, javascript-format +msgid "More than one item match \"%s\". Please narrow down your query." +msgstr "Više od jednog rezultata odgovara \"%s\". Promijeni upit." + +msgid "No notebook selected." +msgstr "Nije odabrana bilježnica." + +msgid "No notebook has been specified." +msgstr "Nije specificirana bilježnica." + +msgid "Y" +msgstr "Y" + +msgid "n" +msgstr "n" + +msgid "N" +msgstr "N" + +msgid "y" +msgstr "y" + +msgid "Cancelling background synchronisation... Please wait." +msgstr "Prekid sinkronizacije u pozadini... Pričekaj." + +#, javascript-format +msgid "No such command: %s" +msgstr "Ne postoji naredba: %s" + +#, javascript-format +msgid "The command \"%s\" is only available in GUI mode" +msgstr "Naredba \"%s\" postoji samo u inačici s grafičkim sučeljem" + +#, javascript-format +msgid "Missing required argument: %s" +msgstr "Nedostaje obavezni argument: %s" + +#, javascript-format +msgid "%s: %s" +msgstr "%s: %s" + +msgid "Your choice: " +msgstr "Tvoj izbor: " + +#, javascript-format +msgid "Invalid answer: %s" +msgstr "Nevažeći odgovor: %s" + +msgid "Attaches the given file to the note." +msgstr "Prilaže datu datoteku bilješci." + +#, javascript-format +msgid "Cannot find \"%s\"." +msgstr "Ne mogu naći \"%s\"." + +msgid "Displays the given note." +msgstr "Prikazuje datu bilješku." + +msgid "Displays the complete information about note." +msgstr "Prikazuje potpunu informaciju o bilješci." + +#, fuzzy +msgid "" +"Gets or sets a config value. If [value] is not provided, it will show the " +"value of [name]. If neither [name] nor [value] is provided, it will list the " +"current configuration." +msgstr "" +"Gets or sets a config value. If [value] is not provided, it will show the " +"value of [name]. If neither [name] nor [value] is provided, it will list the " +"current configuration." + +msgid "Also displays unset and hidden config variables." +msgstr "Također prikazuje nepostavljene i skrivene konfiguracijske varijable." + +#, javascript-format +msgid "%s = %s (%s)" +msgstr "%s = %s (%s)" + +#, javascript-format +msgid "%s = %s" +msgstr "%s = %s" + +#, fuzzy +msgid "" +"Duplicates the notes matching to [notebook]. If no notebook is " +"specified the note is duplicated in the current notebook." +msgstr "" +"Duplicates the notes matching to [notebook]. If no notebook is " +"specified the note is duplicated in the current notebook." + +msgid "Marks a to-do as done." +msgstr "Označava zadatak završenim." + +#, javascript-format +msgid "Note is not a to-do: \"%s\"" +msgstr "Bilješka nije zadatak: \"%s\"" + +msgid "Edit note." +msgstr "Uredi bilješku." + +#, fuzzy +msgid "" +"No text editor is defined. Please set it using `config editor `" +msgstr "" +"Nije definiran program za uređivanje teksta. Postavi ga koristeći `config " +"editor `" + +msgid "No active notebook." +msgstr "Nema aktivne bilježnice." + +#, javascript-format +msgid "Note does not exist: \"%s\". Create it?" +msgstr "Bilješka ne postoji: \"%s\". Napravi je?" + +#, fuzzy +msgid "Starting to edit note. Close the editor to get back to the prompt." +msgstr "" +"Počinjem uređivati bilješku. Za povratak u naredbeni redak, zatvori uređivač." + +msgid "Note has been saved." +msgstr "Bilješka je spremljena." + +msgid "Exits the application." +msgstr "Izlaz iz aplikacije." + +msgid "" +"Exports Joplin data to the given directory. By default, it will export the " +"complete database including notebooks, notes, tags and resources." +msgstr "" +"Izvozi podatke u dati direktorij. Po defaultu izvozi sve podatke uključujući " +"bilježnice, bilješke, zadatke i resurse." + +msgid "Exports only the given note." +msgstr "Izvozi samo datu bilješku." + +msgid "Exports only the given notebook." +msgstr "Izvozi samo datu bilježnicu." + +msgid "Displays a geolocation URL for the note." +msgstr "Prikazuje geolokacijski URL bilješke." + +msgid "Displays usage information." +msgstr "Prikazuje informacije o korištenju." + +msgid "Shortcuts are not available in CLI mode." +msgstr "Prečaci nisu podržani u naredbenom retku." + +msgid "" +"Type `help [command]` for more information about a command; or type `help " +"all` for the complete usage information." +msgstr "" +"Upiši `help [naredba]` za više informacija o naredbi ili `help all` za sve " +"informacije o korištenju." + +msgid "The possible commands are:" +msgstr "Moguće naredbe su:" + +#, fuzzy +msgid "" +"In any command, a note or notebook can be refered to by title or ID, or " +"using the shortcuts `$n` or `$b` for, respectively, the currently selected " +"note or notebook. `$c` can be used to refer to the currently selected item." +msgstr "" +"In any command, a note or notebook can be refered to by title or ID, or " +"using the shortcuts `$n` or `$b` for, respectively, the currently selected " +"note or notebook. `$c` can be used to refer to the currently selected item." + +msgid "To move from one pane to another, press Tab or Shift+Tab." +msgstr "Za prijelaz iz jednog okna u drugo, pritisni Tab ili Shift+Tab." + +#, fuzzy +msgid "" +"Use the arrows and page up/down to scroll the lists and text areas " +"(including this console)." +msgstr "" +"Use the arrows and page up/down to scroll the lists and text areas " +"(including this console)." + +msgid "To maximise/minimise the console, press \"TC\"." +msgstr "Za maksimiziranje/minimiziranje konzole, pritisni \"TC\"." + +msgid "To enter command line mode, press \":\"" +msgstr "Za ulaz u naredbeni redak, pritisni \":\"" + +msgid "To exit command line mode, press ESCAPE" +msgstr "Za izlaz iz naredbenog retka, pritisni Esc" + +msgid "" +"For the complete list of available keyboard shortcuts, type `help shortcuts`" +msgstr "Za potpunu listu mogućih prečaca, upiši `help shortcuts`" + +msgid "Imports an Evernote notebook file (.enex file)." +msgstr "Uvozi Evernote bilježnicu (.enex datoteku)." + +msgid "Do not ask for confirmation." +msgstr "Ne pitaj za potvrdu." + +#, javascript-format +msgid "File \"%s\" will be imported into existing notebook \"%s\". Continue?" +msgstr "" +"Datoteka \"%s\" će biti uvezena u postojeću bilježnicu \"%s\". Nastavi?" + +#, javascript-format +msgid "" +"New notebook \"%s\" will be created and file \"%s\" will be imported into " +"it. Continue?" +msgstr "" +"Nova bilježnica \"%s\" će biti stvorena i datoteka \"%s\" će biti uvezena u " +"nju. Nastavi?" + +#, javascript-format +msgid "Found: %d." +msgstr "Nađeno: %d." + +#, javascript-format +msgid "Created: %d." +msgstr "Stvoreno: %d." + +#, javascript-format +msgid "Updated: %d." +msgstr "Ažurirano: %d." + +#, javascript-format +msgid "Skipped: %d." +msgstr "Preskočeno: %d." + +#, javascript-format +msgid "Resources: %d." +msgstr "Resursi: %d." + +#, javascript-format +msgid "Tagged: %d." +msgstr "Označeno: %d." + +msgid "Importing notes..." +msgstr "Uvozim bilješke..." + +#, javascript-format +msgid "The notes have been imported: %s" +msgstr "Bilješke su uvezene: %s" + +msgid "" +"Displays the notes in the current notebook. Use `ls /` to display the list " +"of notebooks." +msgstr "" +"Prikazuje bilješke u trenutnoj bilježnici. Upiši `ls /` za prikaz liste " +"bilježnica." + +msgid "Displays only the first top notes." +msgstr "Prikaži samo prvih bilješki." + +#, fuzzy +msgid "Sorts the item by (eg. title, updated_time, created_time)." +msgstr "Sorts the item by (eg. title, updated_time, created_time)." + +msgid "Reverses the sorting order." +msgstr "Mijenja redoslijed." + +#, fuzzy +msgid "" +"Displays only the items of the specific type(s). Can be `n` for notes, `t` " +"for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the " +"to-dos, while `-ttd` would display notes and to-dos." +msgstr "" +"Displays only the items of the specific type(s). Can be `n` for notes, `t` " +"for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the " +"to-dos, while `-ttd` would display notes and to-dos." + +msgid "Either \"text\" or \"json\"" +msgstr "Ili \"text\" ili \"json\"" + +#, fuzzy +msgid "" +"Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, " +"TODO_CHECKED (for to-dos), TITLE" +msgstr "" +"Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, " +"TODO_CHECKED (for to-dos), TITLE" + +msgid "Please select a notebook first." +msgstr "Odaberi bilježnicu." + +msgid "Creates a new notebook." +msgstr "Stvara novu bilježnicu." + +msgid "Creates a new note." +msgstr "Stvara novu bilješku." + +msgid "Notes can only be created within a notebook." +msgstr "Bilješke je moguće stvoriti samo u sklopu bilježnice." + +msgid "Creates a new to-do." +msgstr "Stvara novi zadatak." + +msgid "Moves the notes matching to [notebook]." +msgstr "Premješta podudarajuće bilješke u [bilježnicu]." + +#, fuzzy +msgid "Renames the given (note or notebook) to ." +msgstr "Preimenuje datu bilješku ili bilježnicu u ." + +msgid "Deletes the given notebook." +msgstr "Briše datu bilježnicu." + +msgid "Deletes the notebook without asking for confirmation." +msgstr "Briše bilježnicu bez traženja potvrde." + +msgid "Delete notebook? All notes within this notebook will also be deleted." +msgstr "" +"Obrisati bilježnicu? Sve bilješke u toj bilježnici će također biti obrisane." + +#, fuzzy +msgid "Deletes the notes matching ." +msgstr "Briše bilješke koje se podudaraju s ." + +msgid "Deletes the notes without asking for confirmation." +msgstr "Briše bilješke bez traženja potvrde." + +#, javascript-format +msgid "%d notes match this pattern. Delete them?" +msgstr "%d bilješki se podudara s pojmom pretraživanja. Obriši ih?" + +msgid "Delete note?" +msgstr "Obrisati bilješku?" + +#, fuzzy +msgid "Searches for the given in all the notes." +msgstr "Pretražuje dati u svim bilješkama." + +#, fuzzy, javascript-format +msgid "" +"Sets the property of the given to the given [value]. Possible " +"properties are:\n" +"\n" +"%s" +msgstr "" +"Sets the property of the given to the given [value]. Possible " +"properties are:\n" +"\n" +"%s" + +msgid "Displays summary about the notes and notebooks." +msgstr "Prikazuje sažetak o bilješkama i bilježnicama." + +msgid "Synchronises with remote storage." +msgstr "Sinkronizira sa udaljenom pohranom podataka." + +msgid "Sync to provided target (defaults to sync.target config value)" +msgstr "Sinkroniziraj sa metom (default je polje sync.target u konfiguraciji)" + +msgid "Synchronisation is already in progress." +msgstr "Sinkronizacija je već u toku." + +#, javascript-format +msgid "" +"Lock file is already being hold. If you know that no synchronisation is " +"taking place, you may delete the lock file at \"%s\" and resume the " +"operation." +msgstr "" +"Ako sinkronizacija nije u toku, obriši lock datoteku u \"%s\" i nastavi..." + +#, fuzzy +msgid "" +"Authentication was not completed (did not receive an authentication token)." +msgstr "" +"Ovjera nije dovršena (nije dobivena potvrda ovjere - authentication token)." + +#, javascript-format +msgid "Synchronisation target: %s (%s)" +msgstr "Meta sinkronizacije: %s (%s)" + +msgid "Cannot initialize synchroniser." +msgstr "Ne mogu započeti sinkronizaciju." + +msgid "Starting synchronisation..." +msgstr "Započinjem sinkronizaciju..." + +msgid "Cancelling... Please wait." +msgstr "Prekidam... Pričekaj." + +#, fuzzy +msgid "" +" can be \"add\", \"remove\" or \"list\" to assign or remove " +"[tag] from [note], or to list the notes associated with [tag]. The command " +"`tag list` can be used to list all the tags." +msgstr "" +" can be \"add\", \"remove\" or \"list\" to assign or remove " +"[tag] from [note], or to list the notes associated with [tag]. The command " +"`tag list` can be used to list all the tags." + +#, javascript-format +msgid "Invalid command: \"%s\"" +msgstr "Nevažeća naredba: \"%s\"" + +#, fuzzy +msgid "" +" can either be \"toggle\" or \"clear\". Use \"toggle\" to " +"toggle the given to-do between completed and uncompleted state (If the " +"target is a regular note it will be converted to a to-do). Use \"clear\" to " +"convert the to-do back to a regular note." +msgstr "" +" can either be \"toggle\" or \"clear\". Use \"toggle\" to " +"toggle the given to-do between completed and uncompleted state (If the " +"target is a regular note it will be converted to a to-do). Use \"clear\" to " +"convert the to-do back to a regular note." + +msgid "Marks a to-do as non-completed." +msgstr "Označava zadatak kao nezavršen." + +#, fuzzy +msgid "" +"Switches to [notebook] - all further operations will happen within this " +"notebook." +msgstr "" +"Switches to [notebook] - all further operations will happen within this " +"notebook." + +msgid "Displays version information" +msgstr "Prikazuje verziju" + +#, javascript-format +msgid "%s %s (%s)" +msgstr "%s %s (%s)" + +msgid "Enum" +msgstr "Enumeracija" + +#, javascript-format +msgid "Type: %s." +msgstr "Vrsta: %s." + +#, javascript-format +msgid "Possible values: %s." +msgstr "Moguće vrijednosti: %s." + +#, javascript-format +msgid "Default: %s" +msgstr "Default: %s" + +msgid "Possible keys/values:" +msgstr "Mogući ključevi/vrijednosti:" + +msgid "Fatal error:" +msgstr "Fatalna greška:" + +msgid "" +"The application has been authorised - you may now close this browser tab." +msgstr "Aplikacija je autorizirana - smiješ zatvoriti karticu preglednika." + +msgid "The application has been successfully authorised." +msgstr "Aplikacija je uspješno autorizirana." + +msgid "" +"Please open the following URL in your browser to authenticate the " +"application. The application will create a directory in \"Apps/Joplin\" and " +"will only read and write files in this directory. It will have no access to " +"any files outside this directory nor to any other personal data. No data " +"will be shared with any third party." +msgstr "" +"Otvori sljedeći URL u pregledniku da bi ovjerio aplikaciju. Aplikacija će " +"stvoriti direktorij u \"Apps/Joplin\" i koristiti će samo taj direktorij za " +"čitanje i pisanje. Aplikacija neće imati pristup osobnim podacima niti ičemu " +"izvan tog direktorija. Nijedan se podatak neće dijeliti s trećom stranom." + +msgid "Search:" +msgstr "Traži:" + +#, fuzzy +msgid "" +"Welcome to Joplin!\n" +"\n" +"Type `:help shortcuts` for the list of keyboard shortcuts, or just `:help` " +"for usage information.\n" +"\n" +"For example, to create a notebook press `mb`; to create a note press `mn`." +msgstr "" +"Welcome to Joplin!\n" +"\n" +"Type `:help shortcuts` for the list of keyboard shortcuts, or just `:help` " +"for usage information.\n" +"\n" +"For example, to create a notebook press `mb`; to create a note press `mn`." + +msgid "File" +msgstr "Datoteka" + +msgid "New note" +msgstr "Nova bilješka" + +msgid "New to-do" +msgstr "Novi zadatak" + +msgid "New notebook" +msgstr "Nova bilježnica" + +msgid "Import Evernote notes" +msgstr "Uvezi Evernote bilješke" + +msgid "Evernote Export Files" +msgstr "Evernote izvozne datoteke" + +msgid "Quit" +msgstr "Izađi" + +msgid "Edit" +msgstr "Uredi" + +msgid "Copy" +msgstr "Kopiraj" + +msgid "Cut" +msgstr "Izreži" + +msgid "Paste" +msgstr "Zalijepi" + +msgid "Search in all the notes" +msgstr "Pretraži u svim bilješkama" + +msgid "Tools" +msgstr "Alati" + +msgid "Synchronisation status" +msgstr "Status sinkronizacije" + +msgid "Options" +msgstr "Opcije" + +msgid "Help" +msgstr "Pomoć" + +msgid "Website and documentation" +msgstr "Website i dokumentacija" + +msgid "About Joplin" +msgstr "O Joplinu" + +#, javascript-format +msgid "%s %s (%s, %s)" +msgstr "%s %s (%s, %s)" + +msgid "OK" +msgstr "U redu" + +msgid "Cancel" +msgstr "Odustani" + +#, javascript-format +msgid "Notes and settings are stored in: %s" +msgstr "Bilješke i postavke su pohranjene u: %s" + +msgid "Save" +msgstr "Spremi" + +msgid "" +"Disabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent unencrypted to the sync target. Do you wish to " +"continue?" +msgstr "" + +msgid "" +"Enabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent encrypted to the sync target. Do not lose the " +"password as, for security purposes, this will be the *only* way to decrypt " +"the data! To enable encryption, please enter your password below." +msgstr "" + +msgid "Disable encryption" +msgstr "" + +msgid "Enable encryption" +msgstr "" + +msgid "Master Keys" +msgstr "" + +msgid "Active" +msgstr "" + +msgid "ID" +msgstr "ID" + +msgid "Source" +msgstr "Izvor" + +msgid "Created" +msgstr "Stvoreno" + +msgid "Updated" +msgstr "Ažurirano" + +msgid "Password" +msgstr "" + +msgid "Password OK" +msgstr "" + +msgid "" +"Note: Only one master key is going to be used for encryption (the one marked " +"as \"active\"). Any of the keys might be used for decryption, depending on " +"how the notes or notebooks were originally encrypted." +msgstr "" + +msgid "Status" +msgstr "Status" + +msgid "Encryption is:" +msgstr "" + +#, fuzzy +msgid "Enabled" +msgstr "Onemogućeno" + +msgid "Disabled" +msgstr "Onemogućeno" + +msgid "Back" +msgstr "Natrag" + +#, javascript-format +msgid "" +"New notebook \"%s\" will be created and file \"%s\" will be imported into it" +msgstr "" +"Nova bilježnica \"%s\" će biti stvorena i datoteka \"%s\" će biti uvezena u " +"nju" + +msgid "Please create a notebook first." +msgstr "Prvo stvori bilježnicu." + +msgid "Note title:" +msgstr "Naslov bilješke:" + +msgid "Please create a notebook first" +msgstr "Prvo stvori bilježnicu" + +msgid "To-do title:" +msgstr "Naslov zadatka:" + +msgid "Notebook title:" +msgstr "Naslov bilježnice:" + +msgid "Add or remove tags:" +msgstr "Dodaj ili makni oznake:" + +msgid "Separate each tag by a comma." +msgstr "Odvoji oznake zarezom." + +msgid "Rename notebook:" +msgstr "Preimenuj bilježnicu:" + +msgid "Set alarm:" +msgstr "Postavi upozorenje:" + +msgid "Layout" +msgstr "Izgled" + +msgid "Some items cannot be synchronised." +msgstr "Neke stavke se ne mogu sinkronizirati." + +msgid "View them now" +msgstr "Pogledaj ih sada" + +#, fuzzy +msgid "Some items cannot be decrypted." +msgstr "Neke stavke se ne mogu sinkronizirati." + +msgid "Set the password" +msgstr "" + +msgid "Add or remove tags" +msgstr "Dodaj ili makni oznake" + +msgid "Switch between note and to-do type" +msgstr "Zamijeni bilješku i zadatak" + +msgid "Delete" +msgstr "Obriši" + +msgid "Delete notes?" +msgstr "Obriši bilješke?" + +msgid "No notes in here. Create one by clicking on \"New note\"." +msgstr "Ovdje nema bilješki. Stvori novu pritiskom na \"Nova bilješka\"." + +msgid "" +"There is currently no notebook. Create one by clicking on \"New notebook\"." +msgstr "Ovdje nema bilježnica. Stvori novu pritiskom na \"Nova bilježnica\"." + +#, javascript-format +msgid "Unsupported link or message: %s" +msgstr "Nepodržana poveznica ili poruka: %s" + +msgid "Attach file" +msgstr "Priloži datoteku" + +msgid "Set alarm" +msgstr "Postavi upozorenje" + +msgid "Refresh" +msgstr "Osvježi" + +msgid "Clear" +msgstr "Očisti" + +msgid "OneDrive Login" +msgstr "OneDrive Login" + +msgid "Import" +msgstr "Uvoz" + +msgid "Synchronisation Status" +msgstr "Status Sinkronizacije" + +msgid "Encryption Options" +msgstr "" + +msgid "Remove this tag from all the notes?" +msgstr "Makni ovu oznaku iz svih bilješki?" + +msgid "Remove this search from the sidebar?" +msgstr "Makni ovu pretragu iz izbornika?" + +msgid "Rename" +msgstr "Preimenuj" + +msgid "Synchronise" +msgstr "Sinkroniziraj" + +msgid "Notebooks" +msgstr "Bilježnice" + +msgid "Tags" +msgstr "Oznake" + +msgid "Searches" +msgstr "Pretraživanja" + +msgid "Please select where the sync status should be exported to" +msgstr "Odaberi lokaciju za izvoz statusa sinkronizacije" + +#, javascript-format +msgid "Usage: %s" +msgstr "Korištenje: %s" + +#, javascript-format +msgid "Unknown flag: %s" +msgstr "Nepoznata zastavica: %s" + +msgid "File system" +msgstr "Datotečni sustav" + +msgid "OneDrive" +msgstr "OneDrive" + +msgid "OneDrive Dev (For testing only)" +msgstr "OneDrive Dev (Samo za testiranje)" + +#, javascript-format +msgid "Unknown log level: %s" +msgstr "Nepoznata razina logiranja: %s" + +#, javascript-format +msgid "Unknown level ID: %s" +msgstr "Nepoznat ID razine: %s" + +msgid "" +"Cannot refresh token: authentication data is missing. Starting the " +"synchronisation again may fix the problem." +msgstr "Nedostaju podaci za ovjeru. Pokušaj ponovo započeti sinkronizaciju." + +msgid "" +"Could not synchronize with OneDrive.\n" +"\n" +"This error often happens when using OneDrive for Business, which " +"unfortunately cannot be supported.\n" +"\n" +"Please consider using a regular OneDrive account." +msgstr "" +"Ne mogu sinkronizirati OneDrive.\n" +"\n" +"Ova greška se često javlja pri korištenju usluge OneDrive for Business koja " +"nije podržana.\n" +"\n" +"Molimo da koristite obični OneDrive korisnički račun." + +#, javascript-format +msgid "Cannot access %s" +msgstr "Ne mogu pristupiti %s" + +#, javascript-format +msgid "Created local items: %d." +msgstr "Stvorene lokalne stavke: %d." + +#, javascript-format +msgid "Updated local items: %d." +msgstr "Ažurirane lokalne stavke: %d." + +#, javascript-format +msgid "Created remote items: %d." +msgstr "Stvorene udaljene stavke: %d." + +#, javascript-format +msgid "Updated remote items: %d." +msgstr "Ažurirane udaljene stavke: %d." + +#, javascript-format +msgid "Deleted local items: %d." +msgstr "Obrisane lokalne stavke: %d." + +#, javascript-format +msgid "Deleted remote items: %d." +msgstr "Obrisane udaljene stavke: %d." + +#, javascript-format +msgid "State: \"%s\"." +msgstr "Stanje: \"%s\"." + +msgid "Cancelling..." +msgstr "Prekidam..." + +#, javascript-format +msgid "Completed: %s" +msgstr "Dovršeno: %s" + +#, javascript-format +msgid "Synchronisation is already in progress. State: %s" +msgstr "Sinkronizacija je već u toku. Stanje: %s" + +msgid "Conflicts" +msgstr "Sukobi" + +#, javascript-format +msgid "A notebook with this title already exists: \"%s\"" +msgstr "Bilježnica s ovim naslovom već postoji: \"%s\"" + +#, javascript-format +msgid "Notebooks cannot be named \"%s\", which is a reserved title." +msgstr "Naslov \"%s\" je rezerviran i ne može se koristiti." + +msgid "Untitled" +msgstr "Nenaslovljen" + +msgid "This note does not have geolocation information." +msgstr "Ova bilješka nema geolokacijske informacije." + +#, javascript-format +msgid "Cannot copy note to \"%s\" notebook" +msgstr "Ne mogu kopirati bilješku u bilježnicu %s" + +#, javascript-format +msgid "Cannot move note to \"%s\" notebook" +msgstr "Ne mogu premjestiti bilješku u bilježnicu %s" + +msgid "Text editor" +msgstr "Uređivač teksta" + +msgid "" +"The editor that will be used to open a note. If none is provided it will try " +"to auto-detect the default editor." +msgstr "" +"Program za uređivanje koji će biti korišten za uređivanje bilješki. Ako ni " +"jedan nije odabran, pokušati će se sa default programom." + +msgid "Language" +msgstr "Jezik" + +msgid "Date format" +msgstr "Format datuma" + +msgid "Time format" +msgstr "Format vremena" + +msgid "Theme" +msgstr "Tema" + +msgid "Light" +msgstr "Svijetla" + +msgid "Dark" +msgstr "Tamna" + +msgid "Show uncompleted todos on top of the lists" +msgstr "Prikaži nezavršene zadatke na vrhu liste" + +msgid "Save geo-location with notes" +msgstr "Spremi geolokacijske podatke sa bilješkama" + +msgid "Synchronisation interval" +msgstr "Interval sinkronizacije" + +#, javascript-format +msgid "%d minutes" +msgstr "%d minuta" + +#, javascript-format +msgid "%d hour" +msgstr "%d sat" + +#, javascript-format +msgid "%d hours" +msgstr "%d sati" + +msgid "Automatically update the application" +msgstr "Automatsko instaliranje nove verzije" + +msgid "Show advanced options" +msgstr "Prikaži napredne opcije" + +msgid "Synchronisation target" +msgstr "Sinkroniziraj sa" + +msgid "" +"The target to synchonise to. If synchronising with the file system, set " +"`sync.2.path` to specify the target directory." +msgstr "" +"Meta sinkronizacije. U slučaju sinkroniziranja s vlastitim datotečnim " +"sustavom, postavi `sync.2.path` na ciljani direktorij." + +msgid "Directory to synchronise with (absolute path)" +msgstr "Direktorij za sinkroniziranje (apsolutna putanja)" + +msgid "" +"The path to synchronise with when file system synchronisation is enabled. " +"See `sync.target`." +msgstr "" +"Putanja do direktorija za sinkronizaciju u slučaju kad je sinkronizacija sa " +"datotečnim sustavom omogućena. Vidi `sync.target`." + +#, javascript-format +msgid "Invalid option value: \"%s\". Possible values are: %s." +msgstr "Nevažeća vrijednost: \"%s\". Moguće vrijednosti su: %s." + +msgid "Items that cannot be synchronised" +msgstr "Stavke koje se ne mogu sinkronizirati" + +#, javascript-format +msgid "\"%s\": \"%s\"" +msgstr "\"%s\": \"%s\"" + +msgid "Sync status (synced items / total items)" +msgstr "Status (sinkronizirane stavke / ukupni broj stavki)" + +#, javascript-format +msgid "%s: %d/%d" +msgstr "%s: %d/%d" + +#, javascript-format +msgid "Total: %d/%d" +msgstr "Ukupno: %d/%d" + +#, javascript-format +msgid "Conflicted: %d" +msgstr "U sukobu: %d" + +#, javascript-format +msgid "To delete: %d" +msgstr "Za brisanje: %d" + +msgid "Folders" +msgstr "Mape" + +#, fuzzy, javascript-format +msgid "%s: %d notes" +msgstr "%s: %d bilješki" + +msgid "Coming alarms" +msgstr "Nadolazeća upozorenja" + +#, fuzzy, javascript-format +msgid "On %s: %s" +msgstr "On %s: %s" + +msgid "There are currently no notes. Create one by clicking on the (+) button." +msgstr "Trenutno nema bilješki. Stvori novu klikom na (+) gumb." + +msgid "Delete these notes?" +msgstr "Obriši ove bilješke?" + +msgid "Log" +msgstr "Log" + +msgid "Export Debug Report" +msgstr "Izvezi Debug izvještaj" + +msgid "Configuration" +msgstr "Konfiguracija" + +msgid "Move to notebook..." +msgstr "Premjesti u bilježnicu..." + +#, javascript-format +msgid "Move %d notes to notebook \"%s\"?" +msgstr "Premjesti %d bilješke u bilježnicu \"%s\"?" + +msgid "Select date" +msgstr "Odaberi datum" + +msgid "Confirm" +msgstr "Potvrdi" + +msgid "Cancel synchronisation" +msgstr "Prekini sinkronizaciju" + +#, javascript-format +msgid "The notebook could not be saved: %s" +msgstr "Bilježnicu nije moguće snimiti: %s" + +msgid "Edit notebook" +msgstr "Uredi bilježnicu" + +msgid "This note has been modified:" +msgstr "Bilješka je promijenjena:" + +msgid "Save changes" +msgstr "Spremi promjene" + +msgid "Discard changes" +msgstr "Odbaci promjene" + +#, javascript-format +msgid "Unsupported image type: %s" +msgstr "Nepodržana vrsta slike: %s" + +msgid "Attach photo" +msgstr "Priloži sliku" + +msgid "Attach any file" +msgstr "Priloži datoteku" + +msgid "Convert to note" +msgstr "Pretvori u bilješku" + +msgid "Convert to todo" +msgstr "Pretvori u zadatak" + +msgid "Hide metadata" +msgstr "Sakrij metapodatke" + +msgid "Show metadata" +msgstr "Prikaži metapodatke" + +msgid "View on map" +msgstr "Vidi na karti" + +msgid "Delete notebook" +msgstr "Obriši bilježnicu" + +msgid "Login with OneDrive" +msgstr "Prijavi se u OneDrive" + +msgid "" +"Click on the (+) button to create a new note or notebook. Click on the side " +"menu to access your existing notebooks." +msgstr "" +"Klikni (+) gumb za dodavanje nove bilješke ili bilježnice ili odaberi " +"postojeću bilježnicu iz izbornika." + +msgid "You currently have no notebook. Create one by clicking on (+) button." +msgstr "Trenutno nemaš nijednu bilježnicu. Stvori novu klikom na (+) gumb." + +msgid "Welcome" +msgstr "Dobro došli" diff --git a/CliClient/locales/it_IT.po b/CliClient/locales/it_IT.po index 89da79a1a..6da7acc48 100644 --- a/CliClient/locales/it_IT.po +++ b/CliClient/locales/it_IT.po @@ -615,6 +615,70 @@ msgstr "" msgid "Save" msgstr "" +msgid "" +"Disabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent unencrypted to the sync target. Do you wish to " +"continue?" +msgstr "" + +msgid "" +"Enabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent encrypted to the sync target. Do not lose the " +"password as, for security purposes, this will be the *only* way to decrypt " +"the data! To enable encryption, please enter your password below." +msgstr "" + +msgid "Disable encryption" +msgstr "" + +msgid "Enable encryption" +msgstr "" + +msgid "Master Keys" +msgstr "" + +msgid "Active" +msgstr "" + +msgid "ID" +msgstr "" + +msgid "Source" +msgstr "" + +#, fuzzy +msgid "Created" +msgstr "Creato: %d." + +#, fuzzy +msgid "Updated" +msgstr "Aggiornato: %d." + +msgid "Password" +msgstr "" + +msgid "Password OK" +msgstr "" + +msgid "" +"Note: Only one master key is going to be used for encryption (the one marked " +"as \"active\"). Any of the keys might be used for decryption, depending on " +"how the notes or notebooks were originally encrypted." +msgstr "" + +msgid "Status" +msgstr "Stato" + +msgid "Encryption is:" +msgstr "" + +#, fuzzy +msgid "Enabled" +msgstr "Disabilitato" + +msgid "Disabled" +msgstr "Disabilitato" + msgid "Back" msgstr "Indietro" @@ -659,19 +723,12 @@ msgstr "Alcuni elementi non possono essere sincronizzati." msgid "View them now" msgstr "Mostrali ora" -msgid "ID" -msgstr "" - -msgid "Source" -msgstr "" - #, fuzzy -msgid "Created" -msgstr "Creato: %d." +msgid "Some items cannot be decrypted." +msgstr "Alcuni elementi non possono essere sincronizzati." -#, fuzzy -msgid "Updated" -msgstr "Aggiornato: %d." +msgid "Set the password" +msgstr "" msgid "Add or remove tags" msgstr "Aggiungi o rimuovi etichetta" @@ -718,6 +775,9 @@ msgstr "Importa" msgid "Synchronisation Status" msgstr "Stato della Sincronizzazione" +msgid "Encryption Options" +msgstr "" + msgid "Remove this tag from all the notes?" msgstr "Rimuovere questa etichetta da tutte le note?" @@ -895,9 +955,6 @@ msgstr "Salva geo-localizzazione con le note" msgid "Synchronisation interval" msgstr "Intervallo di sincronizzazione" -msgid "Disabled" -msgstr "Disabilitato" - #, javascript-format msgid "%d minutes" msgstr "%d minuti" @@ -990,9 +1047,6 @@ msgstr "Cancellare queste note?" msgid "Log" msgstr "Log" -msgid "Status" -msgstr "Stato" - msgid "Export Debug Report" msgstr "Esporta il Report di Debug" @@ -1077,10 +1131,6 @@ msgstr "" msgid "Welcome" msgstr "Benvenuto" -#, fuzzy -#~ msgid "Some items cannot be decrypted." -#~ msgstr "Alcuni elementi non possono essere sincronizzati." - #~ msgid "Delete notebook?" #~ msgstr "Eliminare il blocco note?" diff --git a/CliClient/locales/ja_JP.po b/CliClient/locales/ja_JP.po new file mode 100644 index 000000000..f8e2937b0 --- /dev/null +++ b/CliClient/locales/ja_JP.po @@ -0,0 +1,1134 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR Laurent Cozic +# This file is distributed under the same license as the Joplin-CLI package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Joplin-CLI 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ja_JP\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.0.5\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgid "Give focus to next pane" +msgstr "次のペインへ" + +msgid "Give focus to previous pane" +msgstr "前のペインへ" + +msgid "Enter command line mode" +msgstr "コマンドラインモードに入る" + +msgid "Exit command line mode" +msgstr "コマンドラインモードの終了" + +msgid "Edit the selected note" +msgstr "選択したノートを編集" + +msgid "Cancel the current command." +msgstr "現在のコマンドをキャンセル" + +msgid "Exit the application." +msgstr "アプリケーションを終了する" + +msgid "Delete the currently selected note or notebook." +msgstr "選択中のノートまたはノートブックを削除" + +msgid "To delete a tag, untag the associated notes." +msgstr "タグを削除するには、関連するノートからタグを外してください。" + +msgid "Please select the note or notebook to be deleted first." +msgstr "ます削除するノートかノートブックを選択してください。" + +msgid "Set a to-do as completed / not completed" +msgstr "ToDoを完了/未完に設定" + +msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible." +msgstr "コンソールを最大表示/最小表示/非表示/可視で切り替える([t][c])" + +msgid "Search" +msgstr "検索" + +msgid "[t]oggle note [m]etadata." +msgstr "ノートのメタ情報を切り替える [tm]" + +msgid "[M]ake a new [n]ote" +msgstr "新しいノートの作成 [mn]" + +msgid "[M]ake a new [t]odo" +msgstr "新しいToDoの作成 [mt]" + +msgid "[M]ake a new note[b]ook" +msgstr "新しいノートブックの作成 [mb]" + +msgid "Copy ([Y]ank) the [n]ote to a notebook." +msgstr "ノートをノートブックにコピー [yn]" + +msgid "Move the note to a notebook." +msgstr "ノートをノートブックに移動" + +msgid "Press Ctrl+D or type \"exit\" to exit the application" +msgstr "アプリケーションを終了するには、Ctrl+Dまたは\"exit\"と入力してください" + +#, javascript-format +msgid "More than one item match \"%s\". Please narrow down your query." +msgstr "" +"一つ以上のアイテムが\"%s\"に一致しました。クエリを絞るようにしてください。" + +msgid "No notebook selected." +msgstr "ノートブックが選択されていません。" + +msgid "No notebook has been specified." +msgstr "ノートブックが選択されていません。" + +msgid "Y" +msgstr "" + +msgid "n" +msgstr "" + +msgid "N" +msgstr "" + +msgid "y" +msgstr "" + +msgid "Cancelling background synchronisation... Please wait." +msgstr "バックグラウンド同期を中止中… しばらくお待ちください。" + +#, javascript-format +msgid "No such command: %s" +msgstr "コマンドが違います:%s" + +#, javascript-format +msgid "The command \"%s\" is only available in GUI mode" +msgstr "コマンド \"%s\"は、GUIのみで有効です。" + +#, javascript-format +msgid "Missing required argument: %s" +msgstr "引数が足りません:%s" + +#, javascript-format +msgid "%s: %s" +msgstr "" + +msgid "Your choice: " +msgstr "選択:" + +#, javascript-format +msgid "Invalid answer: %s" +msgstr "無効な入力:%s" + +msgid "Attaches the given file to the note." +msgstr "選択されたファイルをノートに添付" + +#, javascript-format +msgid "Cannot find \"%s\"." +msgstr "\"%s\"は見つかりませんでした。" + +msgid "Displays the given note." +msgstr "選択されたノートを表示" + +msgid "Displays the complete information about note." +msgstr "ノートに関するすべての情報を表示" + +msgid "" +"Gets or sets a config value. If [value] is not provided, it will show the " +"value of [name]. If neither [name] nor [value] is provided, it will list the " +"current configuration." +msgstr "" +"設定を行います。[value]がない場合は、[name]で示された設定項目の値を表示しま" +"す。両方とも指定されていない場合は、現在の設定のリストを表示します。" + +msgid "Also displays unset and hidden config variables." +msgstr "未設定または非表示の設定項目も表示します。" + +#, javascript-format +msgid "%s = %s (%s)" +msgstr "" + +#, javascript-format +msgid "%s = %s" +msgstr "" + +msgid "" +"Duplicates the notes matching to [notebook]. If no notebook is " +"specified the note is duplicated in the current notebook." +msgstr "" +"に一致するノートを[notebook]に複製します。[notebook]が指定されていない" +"場合は、現在のノートブックに複製を行います。" + +msgid "Marks a to-do as done." +msgstr "ToDoを完了として" + +#, javascript-format +msgid "Note is not a to-do: \"%s\"" +msgstr "ノートはToDoリストではありません:\"%s\"" + +msgid "Edit note." +msgstr "ノートを編集する。" + +msgid "" +"No text editor is defined. Please set it using `config editor `" +msgstr "" +"テキストエディタが設定されていません。`config editor `で設定を" +"行ってください。" + +msgid "No active notebook." +msgstr "有効な\bノートブックがありません。" + +#, javascript-format +msgid "Note does not exist: \"%s\". Create it?" +msgstr "\"%s\"というノートはありません。お作りいたしますか?" + +msgid "Starting to edit note. Close the editor to get back to the prompt." +msgstr "ノートの編集の開始。エディタを閉じると元の画面に戻ることが出来ます。" + +msgid "Note has been saved." +msgstr "ノートは保存されました。" + +msgid "Exits the application." +msgstr "アプリケーションの終了。" + +msgid "" +"Exports Joplin data to the given directory. By default, it will export the " +"complete database including notebooks, notes, tags and resources." +msgstr "" +"Joplinのデータを選択されたディレクトリに出力する。標準では、ノートブック・" +"ノート・タグ・添付データを含むすべてのデータベースを出力します。" + +msgid "Exports only the given note." +msgstr "選択されたノートのみを出力する。" + +msgid "Exports only the given notebook." +msgstr "選択されたノートブックのみを出力する。" + +msgid "Displays a geolocation URL for the note." +msgstr "ノートの位置情報URLを表示する。" + +msgid "Displays usage information." +msgstr "使い方を表示する。" + +msgid "Shortcuts are not available in CLI mode." +msgstr "CLIモードではショートカットは使用できません。" + +msgid "" +"Type `help [command]` for more information about a command; or type `help " +"all` for the complete usage information." +msgstr "" +"コマンドのさらなる情報は、`help [command]`で見ることが出来ます;または、" +"`help all`ですべての使用方法の情報を表示できます。" + +msgid "The possible commands are:" +msgstr "有効なコマンドは:" + +msgid "" +"In any command, a note or notebook can be refered to by title or ID, or " +"using the shortcuts `$n` or `$b` for, respectively, the currently selected " +"note or notebook. `$c` can be used to refer to the currently selected item." +msgstr "" +"すべてのコマンドで、ノートまたはノートブックは、題名またはID、または選択中の" +"物はそれぞれショートカット`$n`または`$b`で指定できます。`$c`で選択中のアイテ" +"ムを参照できます。" + +msgid "To move from one pane to another, press Tab or Shift+Tab." +msgstr "ペイン間を移動するには、TabかShift+Tabをおしてください。" + +msgid "" +"Use the arrows and page up/down to scroll the lists and text areas " +"(including this console)." +msgstr "リストや入力エリアの移動には矢印キーまたはPage Up/Downを使用します。" + +msgid "To maximise/minimise the console, press \"TC\"." +msgstr "コンソールの最大化・最小化には\"TC\"と入力してください。" + +msgid "To enter command line mode, press \":\"" +msgstr "コマンドラインモードに入るには、\":\"を入力してください。" + +msgid "To exit command line mode, press ESCAPE" +msgstr "コマンドラインモードを終了するには、ESCキーを押してください。" + +msgid "" +"For the complete list of available keyboard shortcuts, type `help shortcuts`" +msgstr "" +"有効なすべてのキーボードショートカットを表示するには、`help shortcuts`と入力" +"してください。" + +msgid "Imports an Evernote notebook file (.enex file)." +msgstr "Evernoteノートブックファイル(.enex)のインポート" + +msgid "Do not ask for confirmation." +msgstr "確認を行わない。" + +#, javascript-format +msgid "File \"%s\" will be imported into existing notebook \"%s\". Continue?" +msgstr "" +"ファイル \"%s\" はノートブック \"%s\"に取り込まれます。よろしいですか?" + +#, javascript-format +msgid "" +"New notebook \"%s\" will be created and file \"%s\" will be imported into " +"it. Continue?" +msgstr "" +"新しいノートブック\"%s\"が作成され、ファイル\"%s\"が取り込まれます。よろしい" +"ですか?" + +#, javascript-format +msgid "Found: %d." +msgstr "見つかりました:%d" + +#, javascript-format +msgid "Created: %d." +msgstr "作成しました:%d" + +#, javascript-format +msgid "Updated: %d." +msgstr "アップデートしました:%d" + +#, javascript-format +msgid "Skipped: %d." +msgstr "スキップしました:%d" + +#, javascript-format +msgid "Resources: %d." +msgstr "リソース:%d" + +#, javascript-format +msgid "Tagged: %d." +msgstr "タグ付き:%d" + +msgid "Importing notes..." +msgstr "ノートのインポート…" + +#, javascript-format +msgid "The notes have been imported: %s" +msgstr "ノートはインポートされました:%s" + +msgid "" +"Displays the notes in the current notebook. Use `ls /` to display the list " +"of notebooks." +msgstr "" +"現在のノートブック中のノートを表示します。ノートブックのリストを表示するに" +"は、`ls /`と入力してください。" + +msgid "Displays only the first top notes." +msgstr "上位 件のノートを表示する。" + +msgid "Sorts the item by (eg. title, updated_time, created_time)." +msgstr "アイテムをで並び替え (例: title, updated_time, created_time)." + +msgid "Reverses the sorting order." +msgstr "逆順に並び替える。" + +#, fuzzy +msgid "" +"Displays only the items of the specific type(s). Can be `n` for notes, `t` " +"for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the " +"to-dos, while `-ttd` would display notes and to-dos." +msgstr "" +"選択した種類の項目のみ表示します。`n`でノートを、`t`でToDoを、`nt`でその両方" +"を指定できます。 (例: `-tt` ではToDoのみを表示し、`-ttd`ではノートとToDoを表" +"示します。)" + +msgid "Either \"text\" or \"json\"" +msgstr "\"text\"または\"json\"のどちらか" + +msgid "" +"Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, " +"TODO_CHECKED (for to-dos), TITLE" +msgstr "" +"長い形式のリストフォーマットを使用します。フォーマットは:ID, NOTE_COUNT " +"(ノートブックのみ), DATE, TODO_CHECKED (ToDoのみ), TITLE" + +msgid "Please select a notebook first." +msgstr "ますはノートブックを選択して下さい。" + +msgid "Creates a new notebook." +msgstr "あたらしいノートブックを作成します。" + +msgid "Creates a new note." +msgstr "あたらしいノートを作成します。" + +msgid "Notes can only be created within a notebook." +msgstr "ノートは、ノートブック内のみで作ることが出来ます。" + +msgid "Creates a new to-do." +msgstr "新しいToDoを作成します。" + +msgid "Moves the notes matching to [notebook]." +msgstr "に一致するアイテムを、[notebook]に移動します。" + +msgid "Renames the given (note or notebook) to ." +msgstr " (ノートまたはノートブック)の名前を、に変更します。" + +msgid "Deletes the given notebook." +msgstr "指定されたノートブックを削除します。" + +msgid "Deletes the notebook without asking for confirmation." +msgstr "ノートブックを確認なしで削除します。" + +msgid "Delete notebook? All notes within this notebook will also be deleted." +msgstr "ノートブックを削除しますか?中にあるノートはすべて消えてしまいます。" + +msgid "Deletes the notes matching ." +msgstr "に一致するノートを削除する。" + +msgid "Deletes the notes without asking for confirmation." +msgstr "ノートを確認なしで削除します。" + +#, javascript-format +msgid "%d notes match this pattern. Delete them?" +msgstr "%d個のノートが一致しました。削除しますか?" + +msgid "Delete note?" +msgstr "ノートを削除しますか?" + +msgid "Searches for the given in all the notes." +msgstr "指定されたをすべてのノートから検索する。" + +#, javascript-format +msgid "" +"Sets the property of the given to the given [value]. Possible " +"properties are:\n" +"\n" +"%s" +msgstr "" +"のプロパティ を、指示された[value]に設定します。有効なプロパティ" +"は:\n" +"\n" +"%s" + +msgid "Displays summary about the notes and notebooks." +msgstr "ノートとノートブックのサマリを表示します。" + +msgid "Synchronises with remote storage." +msgstr "リモート保存領域と同期します。" + +msgid "Sync to provided target (defaults to sync.target config value)" +msgstr "指定のターゲットと同期します。(標準: sync.targetの設定値)" + +msgid "Synchronisation is already in progress." +msgstr "同期はすでに実行中です。" + +#, javascript-format +msgid "" +"Lock file is already being hold. If you know that no synchronisation is " +"taking place, you may delete the lock file at \"%s\" and resume the " +"operation." +msgstr "" +"ロックファイルがすでに保持されています。同期作業が行われていない場合は、\"%s" +"\"にあるロックファイルを削除して、作業を再度行ってください。" + +msgid "" +"Authentication was not completed (did not receive an authentication token)." +msgstr "認証は完了していません(認証トークンが得られませんでした)" + +#, javascript-format +msgid "Synchronisation target: %s (%s)" +msgstr "同期先: %s (%s)" + +msgid "Cannot initialize synchroniser." +msgstr "同期プロセスを初期化できませんでした。" + +msgid "Starting synchronisation..." +msgstr "同期を開始中..." + +msgid "Cancelling... Please wait." +msgstr "中止中...お待ちください。" + +msgid "" +" can be \"add\", \"remove\" or \"list\" to assign or remove " +"[tag] from [note], or to list the notes associated with [tag]. The command " +"`tag list` can be used to list all the tags." +msgstr "" +" は\"add\", \"remove\", \"list\"のいずれかで、指定したノートから" +"タグをつけたり外したり出来ます。`tag list`で、すべてのタグを見ることが出来ま" +"す。" + +#, javascript-format +msgid "Invalid command: \"%s\"" +msgstr "無効な命令: \"%s\"" + +msgid "" +" can either be \"toggle\" or \"clear\". Use \"toggle\" to " +"toggle the given to-do between completed and uncompleted state (If the " +"target is a regular note it will be converted to a to-do). Use \"clear\" to " +"convert the to-do back to a regular note." +msgstr "" +"は、\"toggle\"または\"clear\"を指定できます。\"toggle\"を指定す" +"ると、指定したToDoの完了済み/未完を反転できます。指定したノートが通常のノート" +"であれば、ToDoに変換されます。\"clear\"を指定すると、ToDoを通常のノートに変換" +"できます。" + +msgid "Marks a to-do as non-completed." +msgstr "ToDoを未完としてマーク" + +msgid "" +"Switches to [notebook] - all further operations will happen within this " +"notebook." +msgstr "" +"ノートブック [notebook]に切り替え - これ以降の作業は、指定のノートブック内で" +"行われます。" + +msgid "Displays version information" +msgstr "バージョン情報の表示" + +#, javascript-format +msgid "%s %s (%s)" +msgstr "" + +msgid "Enum" +msgstr "列挙" + +#, javascript-format +msgid "Type: %s." +msgstr "種類: %s." + +#, javascript-format +msgid "Possible values: %s." +msgstr "取り得る値: %s." + +#, javascript-format +msgid "Default: %s" +msgstr "規定値: %s" + +msgid "Possible keys/values:" +msgstr "取り得るキーバリュー: " + +msgid "Fatal error:" +msgstr "致命的なエラー: " + +msgid "" +"The application has been authorised - you may now close this browser tab." +msgstr "" +"アプリケーションは認証されました - ブラウザを閉じて頂いてかまいません。" + +msgid "The application has been successfully authorised." +msgstr "アプリケーションは問題なく認証されました。" + +msgid "" +"Please open the following URL in your browser to authenticate the " +"application. The application will create a directory in \"Apps/Joplin\" and " +"will only read and write files in this directory. It will have no access to " +"any files outside this directory nor to any other personal data. No data " +"will be shared with any third party." +msgstr "" +"このアプリケーションを認証するためには下記のURLをブラウザで開いてください。ア" +"プリケーションは\"Apps/Joplin\"フォルダを作成し、その中のファイルのみを読み書" +"きします。あなたの個人的なファイルや、ディレクトリ外のファイルにはアクセスし" +"ません。第三者にデータが共有されることもありません。" + +msgid "Search:" +msgstr "検索: " + +msgid "" +"Welcome to Joplin!\n" +"\n" +"Type `:help shortcuts` for the list of keyboard shortcuts, or just `:help` " +"for usage information.\n" +"\n" +"For example, to create a notebook press `mb`; to create a note press `mn`." +msgstr "" +"Joplinへようこそ!\n" +"\n" +"`:help shortcuts`と入力することで、キーボードショートカットのリストを見ること" +"が出来ます。また、`:help`で使い方を確認できます。\n" +"\n" +"例えば、ノートブックの作成には`mb`で出来、ノートの作成は`mn`で行うことが出来" +"ます。" + +msgid "File" +msgstr "ファイル" + +msgid "New note" +msgstr "新しいノート" + +msgid "New to-do" +msgstr "新しいToDo" + +msgid "New notebook" +msgstr "新しいノートブック" + +msgid "Import Evernote notes" +msgstr "Evernoteのインポート" + +msgid "Evernote Export Files" +msgstr "Evernote Exportファイル" + +msgid "Quit" +msgstr "終了" + +msgid "Edit" +msgstr "編集" + +msgid "Copy" +msgstr "コピー" + +msgid "Cut" +msgstr "切り取り" + +msgid "Paste" +msgstr "貼り付け" + +msgid "Search in all the notes" +msgstr "すべてのノートを検索" + +msgid "Tools" +msgstr "ツール" + +msgid "Synchronisation status" +msgstr "同期状況" + +msgid "Options" +msgstr "オプション" + +msgid "Help" +msgstr "ヘルプ" + +msgid "Website and documentation" +msgstr "Webサイトとドキュメント" + +msgid "About Joplin" +msgstr "Joplinについて" + +#, javascript-format +msgid "%s %s (%s, %s)" +msgstr "" + +msgid "OK" +msgstr "" + +msgid "Cancel" +msgstr "キャンセル" + +#, javascript-format +msgid "Notes and settings are stored in: %s" +msgstr "ノートと設定は、%sに保存されます。" + +msgid "Save" +msgstr "保存" + +msgid "" +"Disabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent unencrypted to the sync target. Do you wish to " +"continue?" +msgstr "" + +msgid "" +"Enabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent encrypted to the sync target. Do not lose the " +"password as, for security purposes, this will be the *only* way to decrypt " +"the data! To enable encryption, please enter your password below." +msgstr "" + +msgid "Disable encryption" +msgstr "" + +msgid "Enable encryption" +msgstr "" + +msgid "Master Keys" +msgstr "" + +msgid "Active" +msgstr "アクティブ" + +msgid "ID" +msgstr "" + +msgid "Source" +msgstr "" + +#, fuzzy +msgid "Created" +msgstr "作成しました:%d" + +#, fuzzy +msgid "Updated" +msgstr "アップデートしました:%d" + +msgid "Password" +msgstr "" + +msgid "Password OK" +msgstr "" + +msgid "" +"Note: Only one master key is going to be used for encryption (the one marked " +"as \"active\"). Any of the keys might be used for decryption, depending on " +"how the notes or notebooks were originally encrypted." +msgstr "" +"注意:\"active\"に指定されたマスターキーのみが暗号化に使用されます。暗号化に" +"使用されたキーの応じて、すべてのキーが暗号解除のために使用されます。" + +msgid "Status" +msgstr "状態" + +msgid "Encryption is:" +msgstr "" + +#, fuzzy +msgid "Enabled" +msgstr "無効" + +msgid "Disabled" +msgstr "無効" + +msgid "Back" +msgstr "戻る" + +#, javascript-format +msgid "" +"New notebook \"%s\" will be created and file \"%s\" will be imported into it" +msgstr "" +"\"%s\"という名前の新しいノートブックが作成され、ファイル\"%s\"がインポートさ" +"れます。" + +msgid "Please create a notebook first." +msgstr "ますはノートブックを作成して下さい。" + +msgid "Note title:" +msgstr "ノートの題名:" + +msgid "Please create a notebook first" +msgstr "ますはノートブックを作成して下さい。" + +msgid "To-do title:" +msgstr "ToDoの題名:" + +msgid "Notebook title:" +msgstr "ノートブックの題名:" + +msgid "Add or remove tags:" +msgstr "タグの追加・削除:" + +msgid "Separate each tag by a comma." +msgstr "それぞれのタグをカンマ(,)で区切ってください。" + +msgid "Rename notebook:" +msgstr "ノートブックの名前を変更:" + +msgid "Set alarm:" +msgstr "アラームをセット:" + +msgid "Layout" +msgstr "レイアウト" + +msgid "Some items cannot be synchronised." +msgstr "いくつかの項目は同期されませんでした。" + +msgid "View them now" +msgstr "今すぐ表示" + +#, fuzzy +msgid "Some items cannot be decrypted." +msgstr "いくつかの項目は同期されませんでした。" + +msgid "Set the password" +msgstr "" + +msgid "Add or remove tags" +msgstr "タグの追加・削除" + +msgid "Switch between note and to-do type" +msgstr "ノートとToDoを切り替え" + +msgid "Delete" +msgstr "削除" + +msgid "Delete notes?" +msgstr "ノートを削除しますか?" + +msgid "No notes in here. Create one by clicking on \"New note\"." +msgstr "ノートがありません。新しいノートを作成して下さい。" + +msgid "" +"There is currently no notebook. Create one by clicking on \"New notebook\"." +msgstr "ノートブックがありません。新しいノートブックを作成してください。" + +#, javascript-format +msgid "Unsupported link or message: %s" +msgstr "" + +msgid "Attach file" +msgstr "ファイルを添付" + +msgid "Set alarm" +msgstr "アラームをセット" + +msgid "Refresh" +msgstr "更新" + +msgid "Clear" +msgstr "クリア" + +msgid "OneDrive Login" +msgstr "OneDriveログイン" + +msgid "Import" +msgstr "インポート" + +msgid "Synchronisation Status" +msgstr "同期状況" + +msgid "Encryption Options" +msgstr "" + +msgid "Remove this tag from all the notes?" +msgstr "すべてのノートからこのタグを削除しますか?" + +msgid "Remove this search from the sidebar?" +msgstr "サイドバーからこの検索を削除しますか?" + +msgid "Rename" +msgstr "名前の変更" + +msgid "Synchronise" +msgstr "同期" + +msgid "Notebooks" +msgstr "ノートブック" + +msgid "Tags" +msgstr "タグ" + +msgid "Searches" +msgstr "検索" + +msgid "Please select where the sync status should be exported to" +msgstr "同期状況の出力先を選択してください" + +#, javascript-format +msgid "Usage: %s" +msgstr "使用方法: %s" + +#, javascript-format +msgid "Unknown flag: %s" +msgstr "不明なフラグ: %s" + +msgid "File system" +msgstr "ファイルシステム" + +msgid "OneDrive" +msgstr "" + +msgid "OneDrive Dev (For testing only)" +msgstr "" + +#, javascript-format +msgid "Unknown log level: %s" +msgstr "" + +#, javascript-format +msgid "Unknown level ID: %s" +msgstr "" + +msgid "" +"Cannot refresh token: authentication data is missing. Starting the " +"synchronisation again may fix the problem." +msgstr "" +"トークンの更新が出来ませんでした。認証データがありません。同期を再度行うこと" +"で解決することがあります。" + +msgid "" +"Could not synchronize with OneDrive.\n" +"\n" +"This error often happens when using OneDrive for Business, which " +"unfortunately cannot be supported.\n" +"\n" +"Please consider using a regular OneDrive account." +msgstr "" +"OneDriveと同期できませんでした。\n" +"\n" +"OneDrive for Business(未サポート)を使用中はこのエラーが起こることがありま" +"す。\n" +"\n" +"通常のOneDriveアカウントの使用をご検討ください。" + +#, javascript-format +msgid "Cannot access %s" +msgstr "%sにアクセスできません" + +#, javascript-format +msgid "Created local items: %d." +msgstr "ローカルアイテムの作成: %d." + +#, javascript-format +msgid "Updated local items: %d." +msgstr "ローカルアイテムの更新: %d." + +#, javascript-format +msgid "Created remote items: %d." +msgstr "リモートアイテムの作成: %d." + +#, javascript-format +msgid "Updated remote items: %d." +msgstr "リモートアイテムの更新: %d." + +#, javascript-format +msgid "Deleted local items: %d." +msgstr "ローカルアイテムの削除: %d." + +#, javascript-format +msgid "Deleted remote items: %d." +msgstr "リモートアイテムの削除: %d." + +#, javascript-format +msgid "State: \"%s\"." +msgstr "状態: \"%s\"。" + +msgid "Cancelling..." +msgstr "中止中..." + +#, javascript-format +msgid "Completed: %s" +msgstr "完了: %s" + +#, javascript-format +msgid "Synchronisation is already in progress. State: %s" +msgstr "同期作業はすでに実行中です。状態: %s" + +msgid "Conflicts" +msgstr "衝突" + +#, javascript-format +msgid "A notebook with this title already exists: \"%s\"" +msgstr "\"%s\"という名前のノートブックはすでに存在しています。" + +#, javascript-format +msgid "Notebooks cannot be named \"%s\", which is a reserved title." +msgstr "" +"\"%s\"と言う名前はシステムで使用するために予約済みです。名前の変更が出来ませ" +"ん。" + +msgid "Untitled" +msgstr "名称未設定" + +msgid "This note does not have geolocation information." +msgstr "このノートには位置情報がありません。" + +#, javascript-format +msgid "Cannot copy note to \"%s\" notebook" +msgstr "ノートをノートブック \"%s\"にコピーできませんでした。" + +#, javascript-format +msgid "Cannot move note to \"%s\" notebook" +msgstr "ノートをノートブック \"%s\"に移動できませんでした。" + +msgid "Text editor" +msgstr "テキストエディタ" + +msgid "" +"The editor that will be used to open a note. If none is provided it will try " +"to auto-detect the default editor." +msgstr "" +"ノートを開くために使用されるエディタです。特に指定がなければ、デフォルトのエ" +"ディタの検出を試みます。" + +msgid "Language" +msgstr "言語" + +msgid "Date format" +msgstr "日付の形式" + +msgid "Time format" +msgstr "時刻の形式" + +msgid "Theme" +msgstr "テーマ" + +msgid "Light" +msgstr "明るい" + +msgid "Dark" +msgstr "暗い" + +msgid "Show uncompleted todos on top of the lists" +msgstr "未完のToDoをリストの上部に表示" + +msgid "Save geo-location with notes" +msgstr "ノートに位置情報を保存" + +msgid "Synchronisation interval" +msgstr "同期間隔" + +#, javascript-format +msgid "%d minutes" +msgstr "%d 分" + +#, javascript-format +msgid "%d hour" +msgstr "%d 時間" + +#, javascript-format +msgid "%d hours" +msgstr "%d 時間" + +msgid "Automatically update the application" +msgstr "アプリケーションの自動更新" + +msgid "Show advanced options" +msgstr "詳細な設定の表示" + +msgid "Synchronisation target" +msgstr "同期先" + +msgid "" +"The target to synchonise to. If synchronising with the file system, set " +"`sync.2.path` to specify the target directory." +msgstr "" +"同期先です。ローカルのファイルシステムと同期する場合は、`sync.2.path`を同期先" +"のディレクトリに設定してください。" + +msgid "Directory to synchronise with (absolute path)" +msgstr "同期先のディレクトリ(絶対パス)" + +msgid "" +"The path to synchronise with when file system synchronisation is enabled. " +"See `sync.target`." +msgstr "" +"ファイルシステム同期の有効時に同期を行うパスです。`sync.target`も参考にしてく" +"ださい。" + +#, javascript-format +msgid "Invalid option value: \"%s\". Possible values are: %s." +msgstr "無効な設定値: \"%s\"。有効な値は: %sです。" + +msgid "Items that cannot be synchronised" +msgstr "同期が出来なかったアイテム" + +#, javascript-format +msgid "\"%s\": \"%s\"" +msgstr "" + +msgid "Sync status (synced items / total items)" +msgstr "同期状況 (同期済/総数)" + +#, javascript-format +msgid "%s: %d/%d" +msgstr "" + +#, javascript-format +msgid "Total: %d/%d" +msgstr "総数: %d/%d" + +#, javascript-format +msgid "Conflicted: %d" +msgstr "衝突: %d" + +#, javascript-format +msgid "To delete: %d" +msgstr "削除予定: %d" + +msgid "Folders" +msgstr "フォルダ" + +#, javascript-format +msgid "%s: %d notes" +msgstr "%s: %d ノート" + +msgid "Coming alarms" +msgstr "時間のきたアラーム" + +#, javascript-format +msgid "On %s: %s" +msgstr "" + +msgid "There are currently no notes. Create one by clicking on the (+) button." +msgstr "ノートがありません。(+)ボタンを押して新しいノートを作成してください。" + +msgid "Delete these notes?" +msgstr "ノートを削除しますか?" + +msgid "Log" +msgstr "ログ" + +msgid "Export Debug Report" +msgstr "デバッグレポートの出力" + +msgid "Configuration" +msgstr "設定" + +msgid "Move to notebook..." +msgstr "ノートブックへ移動..." + +#, javascript-format +msgid "Move %d notes to notebook \"%s\"?" +msgstr "%d個のノートを\"%s\"に移動しますか?" + +msgid "Select date" +msgstr "日付の選択" + +msgid "Confirm" +msgstr "確認" + +msgid "Cancel synchronisation" +msgstr "同期の中止" + +#, javascript-format +msgid "The notebook could not be saved: %s" +msgstr "ノートブックは保存できませんでした:%s" + +msgid "Edit notebook" +msgstr "ノートブックの編集" + +msgid "This note has been modified:" +msgstr "ノートは変更されています:" + +msgid "Save changes" +msgstr "変更を保存" + +msgid "Discard changes" +msgstr "変更を破棄" + +#, javascript-format +msgid "Unsupported image type: %s" +msgstr "サポートされていないイメージ形式: %s." + +msgid "Attach photo" +msgstr "写真を添付" + +msgid "Attach any file" +msgstr "ファイルを添付" + +msgid "Convert to note" +msgstr "ノートに変換" + +msgid "Convert to todo" +msgstr "ToDoに変換" + +msgid "Hide metadata" +msgstr "メタデータを隠す" + +msgid "Show metadata" +msgstr "メタデータを表示" + +msgid "View on map" +msgstr "地図上に表示" + +msgid "Delete notebook" +msgstr "ノートブックを削除" + +msgid "Login with OneDrive" +msgstr "OneDriveログイン" + +msgid "" +"Click on the (+) button to create a new note or notebook. Click on the side " +"menu to access your existing notebooks." +msgstr "" +"(+)ボタンを押してノートやノートブックを作成してください。サイドメニューからあ" +"なたのノートブックにアクセスが出来ます。" + +msgid "You currently have no notebook. Create one by clicking on (+) button." +msgstr "" +"ノートブックがありません。(+)をクリックして新しいノートブックを作成してくださ" +"い。" + +msgid "Welcome" +msgstr "ようこそ" diff --git a/CliClient/locales/joplin.pot b/CliClient/locales/joplin.pot index d805c6765..21fe02e7c 100644 --- a/CliClient/locales/joplin.pot +++ b/CliClient/locales/joplin.pot @@ -559,6 +559,67 @@ msgstr "" msgid "Save" msgstr "" +msgid "" +"Disabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent unencrypted to the sync target. Do you wish to " +"continue?" +msgstr "" + +msgid "" +"Enabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent encrypted to the sync target. Do not lose the " +"password as, for security purposes, this will be the *only* way to decrypt " +"the data! To enable encryption, please enter your password below." +msgstr "" + +msgid "Disable encryption" +msgstr "" + +msgid "Enable encryption" +msgstr "" + +msgid "Master Keys" +msgstr "" + +msgid "Active" +msgstr "" + +msgid "ID" +msgstr "" + +msgid "Source" +msgstr "" + +msgid "Created" +msgstr "" + +msgid "Updated" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Password OK" +msgstr "" + +msgid "" +"Note: Only one master key is going to be used for encryption (the one marked " +"as \"active\"). Any of the keys might be used for decryption, depending on " +"how the notes or notebooks were originally encrypted." +msgstr "" + +msgid "Status" +msgstr "" + +msgid "Encryption is:" +msgstr "" + +msgid "Enabled" +msgstr "" + +msgid "Disabled" +msgstr "" + msgid "Back" msgstr "" @@ -603,16 +664,10 @@ msgstr "" msgid "View them now" msgstr "" -msgid "ID" +msgid "Some items cannot be decrypted." msgstr "" -msgid "Source" -msgstr "" - -msgid "Created" -msgstr "" - -msgid "Updated" +msgid "Set the password" msgstr "" msgid "Add or remove tags" @@ -659,6 +714,9 @@ msgstr "" msgid "Synchronisation Status" msgstr "" +msgid "Encryption Options" +msgstr "" + msgid "Remove this tag from all the notes?" msgstr "" @@ -825,9 +883,6 @@ msgstr "" msgid "Synchronisation interval" msgstr "" -msgid "Disabled" -msgstr "" - #, javascript-format msgid "%d minutes" msgstr "" @@ -915,9 +970,6 @@ msgstr "" msgid "Log" msgstr "" -msgid "Status" -msgstr "" - msgid "Export Debug Report" msgstr "" diff --git a/CliClient/locales/pt_BR.po b/CliClient/locales/pt_BR.po index 3bd66a1cb..709534d86 100644 --- a/CliClient/locales/pt_BR.po +++ b/CliClient/locales/pt_BR.po @@ -610,6 +610,70 @@ msgstr "" msgid "Save" msgstr "" +msgid "" +"Disabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent unencrypted to the sync target. Do you wish to " +"continue?" +msgstr "" + +msgid "" +"Enabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent encrypted to the sync target. Do not lose the " +"password as, for security purposes, this will be the *only* way to decrypt " +"the data! To enable encryption, please enter your password below." +msgstr "" + +msgid "Disable encryption" +msgstr "" + +msgid "Enable encryption" +msgstr "" + +msgid "Master Keys" +msgstr "" + +msgid "Active" +msgstr "" + +msgid "ID" +msgstr "" + +msgid "Source" +msgstr "" + +#, fuzzy +msgid "Created" +msgstr "Criado: %d." + +#, fuzzy +msgid "Updated" +msgstr "Atualizado: %d." + +msgid "Password" +msgstr "" + +msgid "Password OK" +msgstr "" + +msgid "" +"Note: Only one master key is going to be used for encryption (the one marked " +"as \"active\"). Any of the keys might be used for decryption, depending on " +"how the notes or notebooks were originally encrypted." +msgstr "" + +msgid "Status" +msgstr "Status" + +msgid "Encryption is:" +msgstr "" + +#, fuzzy +msgid "Enabled" +msgstr "Desabilitado" + +msgid "Disabled" +msgstr "Desabilitado" + msgid "Back" msgstr "Voltar" @@ -656,19 +720,12 @@ msgstr "Não é possível inicializar o sincronizador." msgid "View them now" msgstr "" -msgid "ID" -msgstr "" - -msgid "Source" -msgstr "" - #, fuzzy -msgid "Created" -msgstr "Criado: %d." +msgid "Some items cannot be decrypted." +msgstr "Não é possível inicializar o sincronizador." -#, fuzzy -msgid "Updated" -msgstr "Atualizado: %d." +msgid "Set the password" +msgstr "" msgid "Add or remove tags" msgstr "Adicionar ou remover tags" @@ -716,6 +773,9 @@ msgstr "Importar" msgid "Synchronisation Status" msgstr "Alvo de sincronização" +msgid "Encryption Options" +msgstr "" + msgid "Remove this tag from all the notes?" msgstr "Remover esta tag de todas as notas?" @@ -894,9 +954,6 @@ msgstr "Salvar geolocalização com notas" msgid "Synchronisation interval" msgstr "Intervalo de sincronização" -msgid "Disabled" -msgstr "Desabilitado" - #, javascript-format msgid "%d minutes" msgstr "%d minutos" @@ -988,9 +1045,6 @@ msgstr "Excluir estas notas?" msgid "Log" msgstr "Log" -msgid "Status" -msgstr "Status" - msgid "Export Debug Report" msgstr "Exportar Relatório de Debug" diff --git a/CliClient/locales/zh_CN.po b/CliClient/locales/zh_CN.po new file mode 100644 index 000000000..2b879e834 --- /dev/null +++ b/CliClient/locales/zh_CN.po @@ -0,0 +1,1093 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR Laurent Cozic +# This file is distributed under the same license as the Joplin-CLI package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Joplin-CLI 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: RCJacH \n" +"Language-Team: RCJacH \n" +"Language: zh-cn\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "Give focus to next pane" +msgstr "聚焦于下个面板" + +msgid "Give focus to previous pane" +msgstr "聚焦于上个面板" + +msgid "Enter command line mode" +msgstr "进入命令行模式" + +msgid "Exit command line mode" +msgstr "退出命令行模式" + +msgid "Edit the selected note" +msgstr "编辑所选笔记" + +msgid "Cancel the current command." +msgstr "取消当前命令。" + +msgid "Exit the application." +msgstr "退出程序。" + +msgid "Delete the currently selected note or notebook." +msgstr "删除当前所选笔记或笔记本。" + +msgid "To delete a tag, untag the associated notes." +msgstr "移除相关笔记的标签后才可删除此标签。" + +msgid "Please select the note or notebook to be deleted first." +msgstr "请选择最先删除的笔记或笔记本。" + +msgid "Set a to-do as completed / not completed" +msgstr "设置待办事项为已完成或未完成" + +msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible." +msgstr "在最大化/最小化/隐藏/显示间切换[t]控制台[c]。" + +msgid "Search" +msgstr "搜索" + +msgid "[t]oggle note [m]etadata." +msgstr "切换[t]笔记元数据[m]。" + +msgid "[M]ake a new [n]ote" +msgstr "创建[M]新笔记[n]" + +msgid "[M]ake a new [t]odo" +msgstr "创建[M]新待办事项[t]" + +msgid "[M]ake a new note[b]ook" +msgstr "创建[M]新笔记本[b]" + +msgid "Copy ([Y]ank) the [n]ote to a notebook." +msgstr "复制[Y]笔记[n]至笔记本。" + +msgid "Move the note to a notebook." +msgstr "移动笔记至笔记本。" + +msgid "Press Ctrl+D or type \"exit\" to exit the application" +msgstr "按Ctrl+D或输入\"exit\"退出程序" + +#, javascript-format +msgid "More than one item match \"%s\". Please narrow down your query." +msgstr "有多个项目与\"%s\"匹配,请缩小您的查询范围。" + +msgid "No notebook selected." +msgstr "未选择笔记本。" + +msgid "No notebook has been specified." +msgstr "无指定笔记本。" + +msgid "Y" +msgstr "是" + +msgid "n" +msgstr "否" + +msgid "N" +msgstr "否" + +msgid "y" +msgstr "是" + +msgid "Cancelling background synchronisation... Please wait." +msgstr "正在取消背景同步...请稍后。" + +#, javascript-format +msgid "No such command: %s" +msgstr "无以下命令:%s" + +#, javascript-format +msgid "The command \"%s\" is only available in GUI mode" +msgstr "命令\"%s\"仅在GUI模式下可用" + +#, javascript-format +msgid "Missing required argument: %s" +msgstr "缺失所需参数:%s" + +#, javascript-format +msgid "%s: %s" +msgstr "%s: %s" + +msgid "Your choice: " +msgstr "您的选择: " + +#, javascript-format +msgid "Invalid answer: %s" +msgstr "此答案无效:%s" + +msgid "Attaches the given file to the note." +msgstr "给笔记附加给定文件。" + +#, javascript-format +msgid "Cannot find \"%s\"." +msgstr "无法找到 \"%s\"。" + +msgid "Displays the given note." +msgstr "显示给定笔记。" + +msgid "Displays the complete information about note." +msgstr "显示关于笔记的全部信息。" + +msgid "" +"Gets or sets a config value. If [value] is not provided, it will show the " +"value of [name]. If neither [name] nor [value] is provided, it will list the " +"current configuration." +msgstr "" +"获取或设置配置变量。若未提供[value],则会显示[name]的值。若[name]及[value]都" +"未提供,则列出当前配置。" + +msgid "Also displays unset and hidden config variables." +msgstr "同时显示未设置的与隐藏的配置变量。" + +#, javascript-format +msgid "%s = %s (%s)" +msgstr "%s = %s (%s)" + +#, javascript-format +msgid "%s = %s" +msgstr "%s = %s" + +msgid "" +"Duplicates the notes matching to [notebook]. If no notebook is " +"specified the note is duplicated in the current notebook." +msgstr "" +"复制符合的笔记至[notebook]。若无指定笔记本则在当前笔记本内复制该笔记。" + +msgid "Marks a to-do as done." +msgstr "标记待办事项为完成。" + +#, javascript-format +msgid "Note is not a to-do: \"%s\"" +msgstr "笔记非待办事项:\"%s\"" + +msgid "Edit note." +msgstr "编辑笔记。" + +msgid "" +"No text editor is defined. Please set it using `config editor `" +msgstr "未定义文本编辑器。请通过 `config editor `设置。" + +msgid "No active notebook." +msgstr "无活动笔记本。" + +#, javascript-format +msgid "Note does not exist: \"%s\". Create it?" +msgstr "此笔记不存在:\"%s\"。是否创建?" + +msgid "Starting to edit note. Close the editor to get back to the prompt." +msgstr "开始编辑笔记。关闭编辑器则返回提示。" + +msgid "Note has been saved." +msgstr "笔记已被保存。" + +msgid "Exits the application." +msgstr "退出程序。" + +msgid "" +"Exports Joplin data to the given directory. By default, it will export the " +"complete database including notebooks, notes, tags and resources." +msgstr "" +"导出Joplin数据至给定文件目录。默认为导出所有的数据库,包含笔记本、笔记、标签" +"及资源。" + +msgid "Exports only the given note." +msgstr "仅导出给定笔记。" + +msgid "Exports only the given notebook." +msgstr "仅导出给定笔记本。" + +msgid "Displays a geolocation URL for the note." +msgstr "显示此笔记的地理定位URL地址。" + +msgid "Displays usage information." +msgstr "显示使用信息。" + +msgid "Shortcuts are not available in CLI mode." +msgstr "快捷键在CLI模式下不可用。" + +#, fuzzy +msgid "" +"Type `help [command]` for more information about a command; or type `help " +"all` for the complete usage information." +msgstr "输入`help [command]`显示更多关于某命令的信息。" + +msgid "The possible commands are:" +msgstr "可用命令为:" + +msgid "" +"In any command, a note or notebook can be refered to by title or ID, or " +"using the shortcuts `$n` or `$b` for, respectively, the currently selected " +"note or notebook. `$c` can be used to refer to the currently selected item." +msgstr "" +"在任意命令中,笔记或笔记本可通过其标题或ID来引用,也可使用代表当前所选笔记或" +"笔记本的变量`$n`与`$b`。`$c`可用于引用当前所选项目。" + +msgid "To move from one pane to another, press Tab or Shift+Tab." +msgstr "按Tab或Shift+Tab切换面板。" + +msgid "" +"Use the arrows and page up/down to scroll the lists and text areas " +"(including this console)." +msgstr "通过上下左右与page up/down键来滚动列表与文本区域(包含此控制台)。" + +msgid "To maximise/minimise the console, press \"TC\"." +msgstr "按\"TC\"最大化/最小化控制台。" + +msgid "To enter command line mode, press \":\"" +msgstr "按\":\"键进入命令行模式" + +msgid "To exit command line mode, press ESCAPE" +msgstr "按ESC键退出命令行模式" + +msgid "" +"For the complete list of available keyboard shortcuts, type `help shortcuts`" +msgstr "输入`help shortcuts`显示全部可用的快捷键列表。" + +msgid "Imports an Evernote notebook file (.enex file)." +msgstr "导入Evernote笔记本文件(.enex文件)。" + +msgid "Do not ask for confirmation." +msgstr "不再要求确认。" + +#, javascript-format +msgid "File \"%s\" will be imported into existing notebook \"%s\". Continue?" +msgstr "文件\"%s\"将会被导入至现有笔记本\"%s\"。是否继续?" + +#, javascript-format +msgid "" +"New notebook \"%s\" will be created and file \"%s\" will be imported into " +"it. Continue?" +msgstr "将创建新笔记本\"%s\"并将文件\"%s\"导入至其中。是否继续?" + +#, javascript-format +msgid "Found: %d." +msgstr "已找到:%d条。" + +#, javascript-format +msgid "Created: %d." +msgstr "已创建:%d条。" + +#, javascript-format +msgid "Updated: %d." +msgstr "已更新:%d条。" + +#, javascript-format +msgid "Skipped: %d." +msgstr "已跳过:%d条。" + +#, javascript-format +msgid "Resources: %d." +msgstr "资源:%d。" + +#, javascript-format +msgid "Tagged: %d." +msgstr "已标签:%d条。" + +msgid "Importing notes..." +msgstr "正在导入笔记..." + +#, javascript-format +msgid "The notes have been imported: %s" +msgstr "以下笔记已被导入:%s" + +msgid "" +"Displays the notes in the current notebook. Use `ls /` to display the list " +"of notebooks." +msgstr "显示当前笔记本的笔记。使用`ls /`显示笔记本列表。" + +msgid "Displays only the first top notes." +msgstr "只显示最上方的条笔记。" + +msgid "Sorts the item by (eg. title, updated_time, created_time)." +msgstr "使用排序项目(例标题、更新日期、创建日期)。" + +msgid "Reverses the sorting order." +msgstr "反转排序顺序。" + +msgid "" +"Displays only the items of the specific type(s). Can be `n` for notes, `t` " +"for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the " +"to-dos, while `-ttd` would display notes and to-dos." +msgstr "" +"仅显示指定格式的项目。`n`代表笔记,`t`代表待办事项,`nt`代表笔记和待办事项" +"(例,`-tt`则会仅显示待办事项,`-ttd`则会显示笔记和待办事项)。" + +msgid "Either \"text\" or \"json\"" +msgstr "\"文本\"或\"json\"" + +msgid "" +"Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, " +"TODO_CHECKED (for to-dos), TITLE" +msgstr "" +"使用长列表格式。格式为ID, NOTE_COUNT(笔记本), DATE, TODO_CHECKED(待办事" +"项),TITLE" + +msgid "Please select a notebook first." +msgstr "请先选择笔记本。" + +msgid "Creates a new notebook." +msgstr "创建新笔记本。" + +msgid "Creates a new note." +msgstr "创建新笔记。" + +msgid "Notes can only be created within a notebook." +msgstr "笔记只能创建于笔记本内。" + +msgid "Creates a new to-do." +msgstr "创建新待办事项。" + +msgid "Moves the notes matching to [notebook]." +msgstr "移动符合的笔记至[notebook]。" + +msgid "Renames the given (note or notebook) to ." +msgstr "重命名给定的(笔记或笔记本)至。" + +msgid "Deletes the given notebook." +msgstr "删除给定笔记本。" + +msgid "Deletes the notebook without asking for confirmation." +msgstr "删除笔记本(不要求确认)。" + +msgid "Delete notebook? All notes within this notebook will also be deleted." +msgstr "" + +msgid "Deletes the notes matching ." +msgstr "删除符合的笔记。" + +msgid "Deletes the notes without asking for confirmation." +msgstr "删除笔记(不要求确认)。" + +#, javascript-format +msgid "%d notes match this pattern. Delete them?" +msgstr "%d条笔记符合此模式。是否删除它们?" + +msgid "Delete note?" +msgstr "是否删除笔记?" + +msgid "Searches for the given in all the notes." +msgstr "在所有笔记内搜索给定的。" + +#, fuzzy, javascript-format +msgid "" +"Sets the property of the given to the given [value]. Possible " +"properties are:\n" +"\n" +"%s" +msgstr "将给定的的属性设置为[value]。" + +msgid "Displays summary about the notes and notebooks." +msgstr "显示关于笔记与笔记本的概况。" + +msgid "Synchronises with remote storage." +msgstr "与远程储存空间同步。" + +msgid "Sync to provided target (defaults to sync.target config value)" +msgstr "同步至所提供的目标(默认为同步目标配置值)" + +msgid "Synchronisation is already in progress." +msgstr "同步正在进行中。" + +#, javascript-format +msgid "" +"Lock file is already being hold. If you know that no synchronisation is " +"taking place, you may delete the lock file at \"%s\" and resume the " +"operation." +msgstr "" +"锁定文件已被保留。若当前没有任何正在进行的同步,您可以在\"%s\"删除锁定文件并" +"继续操作。" + +msgid "" +"Authentication was not completed (did not receive an authentication token)." +msgstr "认证未完成(未收到认证令牌)。" + +#, javascript-format +msgid "Synchronisation target: %s (%s)" +msgstr "同步目标:%s (%s)" + +msgid "Cannot initialize synchroniser." +msgstr "无法初始化同步。" + +msgid "Starting synchronisation..." +msgstr "开始同步..." + +msgid "Cancelling... Please wait." +msgstr "正在取消...请稍后。" + +msgid "" +" can be \"add\", \"remove\" or \"list\" to assign or remove " +"[tag] from [note], or to list the notes associated with [tag]. The command " +"`tag list` can be used to list all the tags." +msgstr "" +"可添加\"add\"、删除\"remove\",或列出\"list\"于[note],用来指定" +"或移除[tag],也可以列出于[tag]相关的笔记。`tag list`命令可用于列出所有标签。" + +#, javascript-format +msgid "Invalid command: \"%s\"" +msgstr "无效命令:\"%s\"" + +msgid "" +" can either be \"toggle\" or \"clear\". Use \"toggle\" to " +"toggle the given to-do between completed and uncompleted state (If the " +"target is a regular note it will be converted to a to-do). Use \"clear\" to " +"convert the to-do back to a regular note." +msgstr "" +"可被切换\"toggle\"或清除\"clear\"。用\"toggle\"可使给定待办事项" +"在已完成与未完成两个状态下切换(若目标为常规笔记,它将被转换成待办事项)。用" +"\"clear\"可把该待办事项转换回常规笔记。" + +msgid "Marks a to-do as non-completed." +msgstr "标记待办事项为未完成。" + +msgid "" +"Switches to [notebook] - all further operations will happen within this " +"notebook." +msgstr "切换至[notebook] - 所有进一步处理将在此笔记本中进行。" + +msgid "Displays version information" +msgstr "显示版本信息。" + +#, javascript-format +msgid "%s %s (%s)" +msgstr "%s %s (%s)" + +msgid "Enum" +msgstr "枚举" + +#, javascript-format +msgid "Type: %s." +msgstr "格式:%s。" + +#, javascript-format +msgid "Possible values: %s." +msgstr "可用值: %s。" + +#, javascript-format +msgid "Default: %s" +msgstr "默认值: %s" + +msgid "Possible keys/values:" +msgstr "可用键/值:" + +msgid "Fatal error:" +msgstr "严重错误:" + +msgid "" +"The application has been authorised - you may now close this browser tab." +msgstr "此程序已被授权 - 您可以关闭此浏览页面了。" + +msgid "The application has been successfully authorised." +msgstr "此程序已被成功授权。" + +msgid "" +"Please open the following URL in your browser to authenticate the " +"application. The application will create a directory in \"Apps/Joplin\" and " +"will only read and write files in this directory. It will have no access to " +"any files outside this directory nor to any other personal data. No data " +"will be shared with any third party." +msgstr "" +"请用网页浏览器打开以下URL来认证此程序。此程序将创建\"Apps/Joplin\"目录,并仅" +"在此目录内写入及读取文件。程序对于在该目录外的文件或任何个人数据没有任何访问" +"权限。同时也不会与第三方共享任何数据。" + +msgid "Search:" +msgstr "搜索:" + +msgid "" +"Welcome to Joplin!\n" +"\n" +"Type `:help shortcuts` for the list of keyboard shortcuts, or just `:help` " +"for usage information.\n" +"\n" +"For example, to create a notebook press `mb`; to create a note press `mn`." +msgstr "" + +msgid "File" +msgstr "文件" + +msgid "New note" +msgstr "新笔记" + +msgid "New to-do" +msgstr "新待办事项" + +msgid "New notebook" +msgstr "新笔记本" + +msgid "Import Evernote notes" +msgstr "导入Evernote笔记" + +msgid "Evernote Export Files" +msgstr "Evernote导出文件" + +msgid "Quit" +msgstr "退出" + +msgid "Edit" +msgstr "编辑" + +msgid "Copy" +msgstr "复制" + +msgid "Cut" +msgstr "剪切" + +msgid "Paste" +msgstr "粘贴" + +msgid "Search in all the notes" +msgstr "在所有笔记内搜索" + +msgid "Tools" +msgstr "工具" + +msgid "Synchronisation status" +msgstr "同步状态" + +msgid "Options" +msgstr "选项" + +msgid "Help" +msgstr "帮助" + +msgid "Website and documentation" +msgstr "网站与文档" + +msgid "About Joplin" +msgstr "关于Joplin" + +#, javascript-format +msgid "%s %s (%s, %s)" +msgstr "%s %s (%s, %s)" + +msgid "OK" +msgstr "确认" + +msgid "Cancel" +msgstr "取消" + +#, javascript-format +msgid "Notes and settings are stored in: %s" +msgstr "" + +msgid "Save" +msgstr "" + +msgid "" +"Disabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent unencrypted to the sync target. Do you wish to " +"continue?" +msgstr "" + +msgid "" +"Enabling encryption means *all* your notes and attachments are going to be " +"re-synchronised and sent encrypted to the sync target. Do not lose the " +"password as, for security purposes, this will be the *only* way to decrypt " +"the data! To enable encryption, please enter your password below." +msgstr "" + +msgid "Disable encryption" +msgstr "" + +msgid "Enable encryption" +msgstr "" + +msgid "Master Keys" +msgstr "" + +msgid "Active" +msgstr "" + +msgid "ID" +msgstr "" + +msgid "Source" +msgstr "" + +#, fuzzy +msgid "Created" +msgstr "已创建:%d条。" + +#, fuzzy +msgid "Updated" +msgstr "已更新:%d条。" + +msgid "Password" +msgstr "" + +msgid "Password OK" +msgstr "" + +msgid "" +"Note: Only one master key is going to be used for encryption (the one marked " +"as \"active\"). Any of the keys might be used for decryption, depending on " +"how the notes or notebooks were originally encrypted." +msgstr "" + +msgid "Status" +msgstr "状态" + +msgid "Encryption is:" +msgstr "" + +#, fuzzy +msgid "Enabled" +msgstr "已禁止" + +msgid "Disabled" +msgstr "已禁止" + +msgid "Back" +msgstr "返回" + +#, javascript-format +msgid "" +"New notebook \"%s\" will be created and file \"%s\" will be imported into it" +msgstr "将创建新笔记本\"%s\"并将文件\"%s\"导入至其中" + +msgid "Please create a notebook first." +msgstr "请先创建笔记本。" + +msgid "Note title:" +msgstr "笔记标题:" + +msgid "Please create a notebook first" +msgstr "请先创建笔记本" + +msgid "To-do title:" +msgstr "待办事项标题:" + +msgid "Notebook title:" +msgstr "笔记本标题:" + +msgid "Add or remove tags:" +msgstr "添加或删除标签:" + +msgid "Separate each tag by a comma." +msgstr "用逗号\",\"分开每个标签。" + +msgid "Rename notebook:" +msgstr "重命名笔记本:" + +msgid "Set alarm:" +msgstr "设置提醒:" + +msgid "Layout" +msgstr "布局" + +msgid "Some items cannot be synchronised." +msgstr "一些项目无法被同步。" + +msgid "View them now" +msgstr "马上查看" + +#, fuzzy +msgid "Some items cannot be decrypted." +msgstr "一些项目无法被同步。" + +msgid "Set the password" +msgstr "" + +msgid "Add or remove tags" +msgstr "添加或删除标签" + +msgid "Switch between note and to-do type" +msgstr "在笔记和待办事项类型之间切换" + +msgid "Delete" +msgstr "删除" + +msgid "Delete notes?" +msgstr "是否删除笔记?" + +msgid "No notes in here. Create one by clicking on \"New note\"." +msgstr "此处无笔记。点击\"新笔记\"创建新笔记。" + +#, fuzzy +msgid "" +"There is currently no notebook. Create one by clicking on \"New notebook\"." +msgstr "当前无笔记。点击(+)创建新笔记。" + +#, javascript-format +msgid "Unsupported link or message: %s" +msgstr "不支持的链接或信息:%s" + +msgid "Attach file" +msgstr "附加文件" + +msgid "Set alarm" +msgstr "设置提醒" + +msgid "Refresh" +msgstr "刷新" + +msgid "Clear" +msgstr "清除" + +msgid "OneDrive Login" +msgstr "登陆OneDrive" + +msgid "Import" +msgstr "导入" + +msgid "Synchronisation Status" +msgstr "同步状态" + +msgid "Encryption Options" +msgstr "" + +msgid "Remove this tag from all the notes?" +msgstr "从所有笔记中删除此标签?" + +msgid "Remove this search from the sidebar?" +msgstr "从侧栏中删除此项搜索历史?" + +msgid "Rename" +msgstr "重命名" + +msgid "Synchronise" +msgstr "同步" + +msgid "Notebooks" +msgstr "笔记本" + +msgid "Tags" +msgstr "标签" + +msgid "Searches" +msgstr "搜索历史" + +#, fuzzy +msgid "Please select where the sync status should be exported to" +msgstr "请选择最先删除的笔记或笔记本。" + +#, javascript-format +msgid "Usage: %s" +msgstr "使用:%s" + +#, javascript-format +msgid "Unknown flag: %s" +msgstr "未知标记:%s" + +msgid "File system" +msgstr "文件系统" + +msgid "OneDrive" +msgstr "OneDrive" + +msgid "OneDrive Dev (For testing only)" +msgstr "OneDrive开发员(仅测试用)" + +#, javascript-format +msgid "Unknown log level: %s" +msgstr "未知日志level:%s" + +#, javascript-format +msgid "Unknown level ID: %s" +msgstr "未知 level ID:%s" + +msgid "" +"Cannot refresh token: authentication data is missing. Starting the " +"synchronisation again may fix the problem." +msgstr "无法刷新令牌:缺失认证数据。请尝试重新启动同步。" + +msgid "" +"Could not synchronize with OneDrive.\n" +"\n" +"This error often happens when using OneDrive for Business, which " +"unfortunately cannot be supported.\n" +"\n" +"Please consider using a regular OneDrive account." +msgstr "" +"无法与OneDrive同步。\n" +"\n" +"此错误经常在使用OneDrive for Business时出现。很可惜我们无法支持此服务。\n" +"\n" +"请您考虑使用常规的OneDrive账号。" + +#, javascript-format +msgid "Cannot access %s" +msgstr "无法访问%s" + +#, javascript-format +msgid "Created local items: %d." +msgstr "已新建本地项目: %d。" + +#, javascript-format +msgid "Updated local items: %d." +msgstr "已更新本地项目: %d。" + +#, javascript-format +msgid "Created remote items: %d." +msgstr "已新建远程项目: %d。" + +#, javascript-format +msgid "Updated remote items: %d." +msgstr "已更新远程项目: %d。" + +#, javascript-format +msgid "Deleted local items: %d." +msgstr "已删除本地项目: %d。" + +#, javascript-format +msgid "Deleted remote items: %d." +msgstr "已删除远程项目: %d。" + +#, javascript-format +msgid "State: \"%s\"." +msgstr "状态:\"%s\"。" + +msgid "Cancelling..." +msgstr "正在取消..." + +#, javascript-format +msgid "Completed: %s" +msgstr "已完成:\"%s\"" + +#, javascript-format +msgid "Synchronisation is already in progress. State: %s" +msgstr "同步正在进行中。状态:%s" + +msgid "Conflicts" +msgstr "冲突" + +#, javascript-format +msgid "A notebook with this title already exists: \"%s\"" +msgstr "以此标题命名的笔记本已存在:\"%s\"" + +#, javascript-format +msgid "Notebooks cannot be named \"%s\", which is a reserved title." +msgstr "笔记本无法被命名为\"%s\",此标题为保留标题。" + +msgid "Untitled" +msgstr "无标题" + +msgid "This note does not have geolocation information." +msgstr "此笔记不包含地理定位信息。" + +#, javascript-format +msgid "Cannot copy note to \"%s\" notebook" +msgstr "无法复制笔记至\"%s\"笔记本" + +#, javascript-format +msgid "Cannot move note to \"%s\" notebook" +msgstr "无法移动笔记至\"%s\"笔记本" + +msgid "Text editor" +msgstr "文本编辑器" + +msgid "" +"The editor that will be used to open a note. If none is provided it will try " +"to auto-detect the default editor." +msgstr "将用于打开笔记的编辑器。若未提供,将自动尝试检测默认编辑器。" + +msgid "Language" +msgstr "语言" + +msgid "Date format" +msgstr "日期格式" + +msgid "Time format" +msgstr "时间格式" + +msgid "Theme" +msgstr "主题" + +msgid "Light" +msgstr "浅色" + +msgid "Dark" +msgstr "深色" + +msgid "Show uncompleted todos on top of the lists" +msgstr "在列表上方显示未完成的待办事项" + +msgid "Save geo-location with notes" +msgstr "保存笔记时同时保存地理定位信息" + +msgid "Synchronisation interval" +msgstr "同步间隔" + +#, javascript-format +msgid "%d minutes" +msgstr "%d分" + +#, javascript-format +msgid "%d hour" +msgstr "%d小时" + +#, javascript-format +msgid "%d hours" +msgstr "%d小时" + +msgid "Automatically update the application" +msgstr "自动更新此程序" + +msgid "Show advanced options" +msgstr "显示高级选项" + +msgid "Synchronisation target" +msgstr "同步目标" + +msgid "" +"The target to synchonise to. If synchronising with the file system, set " +"`sync.2.path` to specify the target directory." +msgstr "同步的目标。若与文件系统同步,设置`sync.2.path`为指定目标目录。" + +msgid "Directory to synchronise with (absolute path)" +msgstr "" + +msgid "" +"The path to synchronise with when file system synchronisation is enabled. " +"See `sync.target`." +msgstr "当文件系统同步开启时的同步路径。参考`sync.target`。" + +#, javascript-format +msgid "Invalid option value: \"%s\". Possible values are: %s." +msgstr "无效的选项值:\"%s\"。可用值为:%s。" + +msgid "Items that cannot be synchronised" +msgstr "项目无法被同步。" + +#, javascript-format +msgid "\"%s\": \"%s\"" +msgstr "\"%s\": \"%s\"" + +msgid "Sync status (synced items / total items)" +msgstr "同步状态(已同步项目/项目总数)" + +#, javascript-format +msgid "%s: %d/%d" +msgstr "%s:%d/%d条" + +#, javascript-format +msgid "Total: %d/%d" +msgstr "总数:%d/%d条" + +#, javascript-format +msgid "Conflicted: %d" +msgstr "有冲突的:%d条" + +#, javascript-format +msgid "To delete: %d" +msgstr "将删除:%d条" + +msgid "Folders" +msgstr "文件夹" + +#, javascript-format +msgid "%s: %d notes" +msgstr "%s: %d条笔记" + +msgid "Coming alarms" +msgstr "临近提醒" + +#, javascript-format +msgid "On %s: %s" +msgstr "%s:%s" + +msgid "There are currently no notes. Create one by clicking on the (+) button." +msgstr "当前无笔记。点击(+)创建新笔记。" + +msgid "Delete these notes?" +msgstr "是否删除这些笔记?" + +msgid "Log" +msgstr "日志" + +msgid "Export Debug Report" +msgstr "导出调试报告" + +msgid "Configuration" +msgstr "配置" + +msgid "Move to notebook..." +msgstr "移动至笔记本..." + +#, javascript-format +msgid "Move %d notes to notebook \"%s\"?" +msgstr "移动%d条笔记至笔记本\"%s\"?" + +msgid "Select date" +msgstr "选择日期" + +msgid "Confirm" +msgstr "确认" + +msgid "Cancel synchronisation" +msgstr "取消同步" + +#, javascript-format +msgid "The notebook could not be saved: %s" +msgstr "此笔记本无法保存:%s" + +msgid "Edit notebook" +msgstr "编辑笔记本" + +msgid "This note has been modified:" +msgstr "此笔记已被修改:" + +msgid "Save changes" +msgstr "保存更改" + +msgid "Discard changes" +msgstr "放弃更改" + +#, javascript-format +msgid "Unsupported image type: %s" +msgstr "不支持的图片格式:%s" + +msgid "Attach photo" +msgstr "附加照片" + +msgid "Attach any file" +msgstr "附加任何文件" + +msgid "Convert to note" +msgstr "转换至笔记" + +msgid "Convert to todo" +msgstr "转换至待办事项" + +msgid "Hide metadata" +msgstr "隐藏元数据" + +msgid "Show metadata" +msgstr "显示元数据" + +msgid "View on map" +msgstr "查看地图" + +msgid "Delete notebook" +msgstr "删除笔记本" + +msgid "Login with OneDrive" +msgstr "用OneDrive登陆" + +msgid "" +"Click on the (+) button to create a new note or notebook. Click on the side " +"menu to access your existing notebooks." +msgstr "点击(+)按钮创建新笔记或笔记本。点击侧边菜单来访问您现有的笔记本。" + +msgid "You currently have no notebook. Create one by clicking on (+) button." +msgstr "您当前没有任何笔记本。点击(+)按钮创建新笔记本。" + +msgid "Welcome" +msgstr "欢迎" + +#~ msgid "Delete notebook \"%s\"?" +#~ msgstr "删除笔记本\"%s\"?" + +#~ msgid "Delete notebook?" +#~ msgstr "是否删除笔记本?" + +#~ msgid "File system synchronisation target directory" +#~ msgstr "文件系统同步目标目录" diff --git a/CliClient/package-lock.json b/CliClient/package-lock.json index 8e0f59ec7..8341f5b49 100644 --- a/CliClient/package-lock.json +++ b/CliClient/package-lock.json @@ -1,6 +1,6 @@ { "name": "joplin", - "version": "0.10.83", + "version": "0.10.86", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -411,12 +411,12 @@ } }, "fs-extra": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", - "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", + "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", "requires": { "graceful-fs": "4.1.11", - "jsonfile": "3.0.1", + "jsonfile": "4.0.0", "universalify": "0.1.1" } }, @@ -728,9 +728,9 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "jsonfile": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", - "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "requires": { "graceful-fs": "4.1.11" } diff --git a/CliClient/package.json b/CliClient/package.json index 0f6b6a18f..0acb512fa 100644 --- a/CliClient/package.json +++ b/CliClient/package.json @@ -14,11 +14,12 @@ "title": "Joplin CLI", "years": [ 2016, - 2017 + 2017, + 2018 ], "owner": "Laurent Cozic" }, - "version": "0.10.83", + "version": "0.10.86", "bin": { "joplin": "./main.js" }, @@ -29,7 +30,7 @@ "app-module-path": "^2.2.0", "follow-redirects": "^1.2.4", "form-data": "^2.1.4", - "fs-extra": "^3.0.1", + "fs-extra": "^5.0.0", "html-entities": "^1.2.1", "jssha": "^2.3.0", "levenshtein": "^1.0.5", diff --git a/CliClient/run.sh b/CliClient/run.sh index eeb6dc569..43f11e2a3 100755 --- a/CliClient/run.sh +++ b/CliClient/run.sh @@ -4,4 +4,4 @@ CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" bash "$CLIENT_DIR/build.sh" && node "$CLIENT_DIR/build/main.js" --profile ~/Temp/TestNotes2 --stack-trace-enabled --log-level debug --env dev "$@" -#bash $CLIENT_DIR/build.sh && NODE_PATH="$CLIENT_DIR/build/" node build/main.js --profile ~/Temp/TestNotes2 --stack-trace-enabled --log-level debug --env dev "$@" \ No newline at end of file +# bash $CLIENT_DIR/build.sh && NODE_PATH="$CLIENT_DIR/build/" node build/main.js --profile ~/.config/joplin --stack-trace-enabled --log-level debug "$@" \ No newline at end of file diff --git a/CliClient/run_test.sh b/CliClient/run_test.sh index 45540c425..c21324965 100755 --- a/CliClient/run_test.sh +++ b/CliClient/run_test.sh @@ -1,10 +1,17 @@ #!/bin/bash ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" BUILD_DIR="$ROOT_DIR/tests-build" +TEST_FILE="$1" rsync -a --exclude "node_modules/" "$ROOT_DIR/tests/" "$BUILD_DIR/" rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/" rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/" mkdir -p "$BUILD_DIR/data" -(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js) \ No newline at end of file +if [[ $TEST_FILE == "" ]]; then + (cd "$ROOT_DIR" && npm test tests-build/synchronizer.js) + (cd "$ROOT_DIR" && npm test tests-build/encryption.js) + (cd "$ROOT_DIR" && npm test tests-build/ArrayUtils.js) +else + (cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js) +fi \ No newline at end of file diff --git a/CliClient/tests/ArrayUtils.js b/CliClient/tests/ArrayUtils.js new file mode 100644 index 000000000..c894b3d94 --- /dev/null +++ b/CliClient/tests/ArrayUtils.js @@ -0,0 +1,32 @@ +require('app-module-path').addPath(__dirname); + +const { time } = require('lib/time-utils.js'); +const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js'); +const ArrayUtils = require('lib/ArrayUtils.js'); + +process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); +}); + +describe('Encryption', function() { + + beforeEach(async (done) => { + done(); + }); + + it('should remove array elements', async (done) => { + let a = ['un', 'deux', 'trois']; + a = ArrayUtils.removeElement(a, 'deux'); + + expect(a[0]).toBe('un'); + expect(a[1]).toBe('trois'); + expect(a.length).toBe(2); + + a = ['un', 'deux', 'trois']; + a = ArrayUtils.removeElement(a, 'not in there'); + expect(a.length).toBe(3); + + done(); + }); + +}); \ No newline at end of file diff --git a/CliClient/tests/encryption.js b/CliClient/tests/encryption.js new file mode 100644 index 000000000..63ca4a417 --- /dev/null +++ b/CliClient/tests/encryption.js @@ -0,0 +1,180 @@ +require('app-module-path').addPath(__dirname); + +const { time } = require('lib/time-utils.js'); +const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); +const Tag = require('lib/models/Tag.js'); +const { Database } = require('lib/database.js'); +const Setting = require('lib/models/Setting.js'); +const BaseItem = require('lib/models/BaseItem.js'); +const BaseModel = require('lib/BaseModel.js'); +const MasterKey = require('lib/models/MasterKey'); +const SyncTargetRegistry = require('lib/SyncTargetRegistry.js'); +const EncryptionService = require('lib/services/EncryptionService.js'); + +process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); +}); + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; // The first test is slow because the database needs to be built + +let service = null; + +describe('Encryption', function() { + + beforeEach(async (done) => { + await setupDatabaseAndSynchronizer(1); + //await setupDatabaseAndSynchronizer(2); + //await switchClient(1); + service = new EncryptionService(); + BaseItem.encryptionService_ = service; + Setting.setValue('encryption.enabled', true); + done(); + }); + + it('should encode and decode header', async (done) => { + const header = { + encryptionMethod: EncryptionService.METHOD_SJCL, + masterKeyId: '01234568abcdefgh01234568abcdefgh', + }; + + const encodedHeader = service.encodeHeader_(header); + const decodedHeader = service.decodeHeader_(encodedHeader); + delete decodedHeader.length; + + expect(objectsEqual(header, decodedHeader)).toBe(true); + + done(); + }); + + it('should generate and decrypt a master key', async (done) => { + const masterKey = await service.generateMasterKey('123456'); + expect(!!masterKey.checksum).toBe(true); + expect(!!masterKey.content).toBe(true); + + let hasThrown = false; + try { + await service.decryptMasterKey(masterKey, 'wrongpassword'); + } catch (error) { + hasThrown = true; + } + + expect(hasThrown).toBe(true); + + const decryptedMasterKey = await service.decryptMasterKey(masterKey, '123456'); + expect(decryptedMasterKey.length).toBe(512); + + done(); + }); + + it('should encrypt and decrypt with a master key', async (done) => { + let masterKey = await service.generateMasterKey('123456'); + masterKey = await MasterKey.save(masterKey); + + await service.loadMasterKey(masterKey, '123456', true); + + const cipherText = await service.encryptString('some secret'); + const plainText = await service.decryptString(cipherText); + + expect(plainText).toBe('some secret'); + + // Test that a long string, that is going to be split into multiple chunks, encrypt + // and decrypt properly too. + let veryLongSecret = ''; + for (let i = 0; i < service.chunkSize() * 3; i++) veryLongSecret += Math.floor(Math.random() * 9); + + const cipherText2 = await service.encryptString(veryLongSecret); + const plainText2 = await service.decryptString(cipherText2); + + expect(plainText2 === veryLongSecret).toBe(true); + + done(); + }); + + it('should fail to decrypt if master key not present', async (done) => { + let masterKey = await service.generateMasterKey('123456'); + masterKey = await MasterKey.save(masterKey); + + await service.loadMasterKey(masterKey, '123456', true); + + const cipherText = await service.encryptString('some secret'); + + await service.unloadMasterKey(masterKey); + + let hasThrown = await checkThrowAsync(async () => await service.decryptString(cipherText)); + + expect(hasThrown).toBe(true); + + done(); + }); + + + it('should fail to decrypt if data tampered with', async (done) => { + let masterKey = await service.generateMasterKey('123456'); + masterKey = await MasterKey.save(masterKey); + + await service.loadMasterKey(masterKey, '123456', true); + + let cipherText = await service.encryptString('some secret'); + cipherText += "ABCDEFGHIJ"; + + let hasThrown = await checkThrowAsync(async () => await service.decryptString(cipherText)); + + expect(hasThrown).toBe(true); + + done(); + }); + + it('should encrypt and decrypt notes and folders', async (done) => { + let masterKey = await service.generateMasterKey('123456'); + masterKey = await MasterKey.save(masterKey); + await service.loadMasterKey(masterKey, '123456', true); + + let folder = await Folder.save({ title: 'folder' }); + let note = await Note.save({ title: 'encrypted note', body: 'something', parent_id: folder.id }); + let serialized = await Note.serializeForSync(note); + let deserialized = Note.filter(await Note.unserialize(serialized)); + + // Check that required properties are not encrypted + expect(deserialized.id).toBe(note.id); + expect(deserialized.parent_id).toBe(note.parent_id); + expect(deserialized.updated_time).toBe(note.updated_time); + + // Check that at least title and body are encrypted + expect(!deserialized.title).toBe(true); + expect(!deserialized.body).toBe(true); + + // Check that encrypted data is there + expect(!!deserialized.encryption_cipher_text).toBe(true); + + encryptedNote = await Note.save(deserialized); + decryptedNote = await Note.decrypt(encryptedNote); + + expect(decryptedNote.title).toBe(note.title); + expect(decryptedNote.body).toBe(note.body); + expect(decryptedNote.id).toBe(note.id); + expect(decryptedNote.parent_id).toBe(note.parent_id); + + done(); + }); + + it('should encrypt and decrypt files', async (done) => { + let masterKey = await service.generateMasterKey('123456'); + masterKey = await MasterKey.save(masterKey); + await service.loadMasterKey(masterKey, '123456', true); + + const sourcePath = __dirname + '/../tests/support/photo.jpg'; + const encryptedPath = __dirname + '/data/photo.crypted'; + const decryptedPath = __dirname + '/data/photo.jpg'; + + await service.encryptFile(sourcePath, encryptedPath); + await service.decryptFile(encryptedPath, decryptedPath); + + expect(fileContentEqual(sourcePath, encryptedPath)).toBe(false); + expect(fileContentEqual(sourcePath, decryptedPath)).toBe(true); + + done(); + }); + +}); \ No newline at end of file diff --git a/CliClient/tests/support/photo.jpg b/CliClient/tests/support/photo.jpg new file mode 100644 index 000000000..b258679de Binary files /dev/null and b/CliClient/tests/support/photo.jpg differ diff --git a/CliClient/tests/synchronizer.js b/CliClient/tests/synchronizer.js index 0351e9d33..99fee8a3e 100644 --- a/CliClient/tests/synchronizer.js +++ b/CliClient/tests/synchronizer.js @@ -1,14 +1,18 @@ require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); -const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId } = require('test-utils.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); -const { Tag } = require('lib/models/tag.js'); +const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, checkThrowAsync, asyncTest } = require('test-utils.js'); +const { shim } = require('lib/shim.js'); +const fs = require('fs-extra'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); +const Resource = require('lib/models/Resource.js'); +const Tag = require('lib/models/Tag.js'); const { Database } = require('lib/database.js'); -const { Setting } = require('lib/models/setting.js'); -const { BaseItem } = require('lib/models/base-item.js'); -const { BaseModel } = require('lib/base-model.js'); +const Setting = require('lib/models/Setting.js'); +const MasterKey = require('lib/models/MasterKey'); +const BaseItem = require('lib/models/BaseItem.js'); +const BaseModel = require('lib/BaseModel.js'); const SyncTargetRegistry = require('lib/SyncTargetRegistry.js'); process.on('unhandledRejection', (reason, p) => { @@ -23,6 +27,38 @@ async function allItems() { return folders.concat(notes); } +async function allSyncTargetItemsEncrypted() { + const list = await fileApi().list(); + const files = list.items; + + //console.info(Setting.value('resourceDir')); + + let totalCount = 0; + let encryptedCount = 0; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const remoteContentString = await fileApi().get(file.path); + const remoteContent = await BaseItem.unserialize(remoteContentString); + const ItemClass = BaseItem.itemClass(remoteContent); + + if (!ItemClass.encryptionSupported()) continue; + + totalCount++; + + if (remoteContent.type_ === BaseModel.TYPE_RESOURCE) { + const content = await fileApi().get('.resource/' + remoteContent.id); + totalCount++; + if (content.substr(0, 5) === 'JED01') output = encryptedCount++; + } + + if (!!remoteContent.encryption_applied) encryptedCount++; + } + + if (!totalCount) throw new Error('No encryptable item on sync target'); + + return totalCount === encryptedCount; +} + async function localItemsSameAsRemote(locals, expect) { try { let files = await fileApi().list(); @@ -53,16 +89,22 @@ async function localItemsSameAsRemote(locals, expect) { } } +let insideBeforeEach = false; + describe('Synchronizer', function() { - beforeEach( async (done) => { + beforeEach(async (done) => { + insideBeforeEach = true; + await setupDatabaseAndSynchronizer(1); await setupDatabaseAndSynchronizer(2); await switchClient(1); done(); + + insideBeforeEach = false; }); - it('should create remote items', async (done) => { + it('should create remote items', asyncTest(async () => { let folder = await Folder.save({ title: "folder1" }); await Note.save({ title: "un", parent_id: folder.id }); @@ -71,11 +113,9 @@ describe('Synchronizer', function() { await synchronizer().start(); await localItemsSameAsRemote(all, expect); + })); - done(); - }); - - it('should update remote item', async (done) => { + it('should update remote item', asyncTest(async () => { let folder = await Folder.save({ title: "folder1" }); let note = await Note.save({ title: "un", parent_id: folder.id }); await synchronizer().start(); @@ -86,11 +126,9 @@ describe('Synchronizer', function() { await synchronizer().start(); await localItemsSameAsRemote(all, expect); + })); - done(); - }); - - it('should create local items', async (done) => { + it('should create local items', asyncTest(async () => { let folder = await Folder.save({ title: "folder1" }); await Note.save({ title: "un", parent_id: folder.id }); await synchronizer().start(); @@ -102,11 +140,9 @@ describe('Synchronizer', function() { let all = await allItems(); await localItemsSameAsRemote(all, expect); + })); - done(); - }); - - it('should update local items', async (done) => { + it('should update local items', asyncTest(async () => { let folder1 = await Folder.save({ title: "folder1" }); let note1 = await Note.save({ title: "un", parent_id: folder1.id }); await synchronizer().start(); @@ -131,11 +167,9 @@ describe('Synchronizer', function() { let all = await allItems(); await localItemsSameAsRemote(all, expect); + })); - done(); - }); - - it('should resolve note conflicts', async (done) => { + it('should resolve note conflicts', asyncTest(async () => { let folder1 = await Folder.save({ title: "folder1" }); let note1 = await Note.save({ title: "un", parent_id: folder1.id }); await synchronizer().start(); @@ -174,11 +208,9 @@ describe('Synchronizer', function() { if (!noteUpdatedFromRemote.hasOwnProperty(n)) continue; expect(noteUpdatedFromRemote[n]).toBe(note2[n], 'Property: ' + n); } + })); - done(); - }); - - it('should resolve folders conflicts', async (done) => { + it('should resolve folders conflicts', asyncTest(async () => { let folder1 = await Folder.save({ title: "folder1" }); let note1 = await Note.save({ title: "un", parent_id: folder1.id }); await synchronizer().start(); @@ -209,11 +241,9 @@ describe('Synchronizer', function() { let folder1_final = await Folder.load(folder1.id); expect(folder1_final.title).toBe(folder1_modRemote.title); + })); - done(); - }); - - it('should delete remote notes', async (done) => { + it('should delete remote notes', asyncTest(async () => { let folder1 = await Folder.save({ title: "folder1" }); let note1 = await Note.save({ title: "un", parent_id: folder1.id }); await synchronizer().start(); @@ -236,11 +266,9 @@ describe('Synchronizer', function() { let deletedItems = await BaseItem.deletedItems(syncTargetId()); expect(deletedItems.length).toBe(0); + })); - done(); - }); - - it('should delete local notes', async (done) => { + it('should delete local notes', asyncTest(async () => { let folder1 = await Folder.save({ title: "folder1" }); let note1 = await Note.save({ title: "un", parent_id: folder1.id }); await synchronizer().start(); @@ -258,11 +286,9 @@ describe('Synchronizer', function() { expect(items.length).toBe(1); let deletedItems = await BaseItem.deletedItems(syncTargetId()); expect(deletedItems.length).toBe(0); - - done(); - }); + })); - it('should delete remote folder', async (done) => { + it('should delete remote folder', asyncTest(async () => { let folder1 = await Folder.save({ title: "folder1" }); let folder2 = await Folder.save({ title: "folder2" }); await synchronizer().start(); @@ -279,11 +305,9 @@ describe('Synchronizer', function() { let all = await allItems(); localItemsSameAsRemote(all, expect); - - done(); - }); + })); - it('should delete local folder', async (done) => { + it('should delete local folder', asyncTest(async () => { let folder1 = await Folder.save({ title: "folder1" }); let folder2 = await Folder.save({ title: "folder2" }); await synchronizer().start(); @@ -304,11 +328,9 @@ describe('Synchronizer', function() { let items = await allItems(); localItemsSameAsRemote(items, expect); - - done(); - }); + })); - it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', async (done) => { + it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', asyncTest(async () => { let folder1 = await Folder.save({ title: "folder1" }); await synchronizer().start(); @@ -326,11 +348,9 @@ describe('Synchronizer', function() { expect(items.length).toBe(1); expect(items[0].title).toBe('note1'); expect(items[0].is_conflict).toBe(1); - - done(); - }); + })); - it('should resolve conflict if note has been deleted remotely and locally', async (done) => { + it('should resolve conflict if note has been deleted remotely and locally', asyncTest(async () => { let folder = await Folder.save({ title: "folder" }); let note = await Note.save({ title: "note", parent_id: folder.title }); await synchronizer().start(); @@ -351,11 +371,9 @@ describe('Synchronizer', function() { expect(items[0].title).toBe('folder'); localItemsSameAsRemote(items, expect); - - done(); - }); + })); - it('should cross delete all folders', async (done) => { + it('should cross delete all folders', asyncTest(async () => { // If client1 and 2 have two folders, client 1 deletes item 1 and client // 2 deletes item 2, they should both end up with no items after sync. @@ -391,11 +409,9 @@ describe('Synchronizer', function() { expect(items1.length).toBe(0); expect(items1.length).toBe(items2.length); - - done(); - }); + })); - it('should handle conflict when remote note is deleted then local note is modified', async (done) => { + it('should handle conflict when remote note is deleted then local note is modified', asyncTest(async () => { let folder1 = await Folder.save({ title: "folder1" }); let note1 = await Note.save({ title: "un", parent_id: folder1.id }); await synchronizer().start(); @@ -425,11 +441,9 @@ describe('Synchronizer', function() { let unconflictedNotes = await Note.unconflictedNotes(); expect(unconflictedNotes.length).toBe(0); - - done(); - }); + })); - it('should handle conflict when remote folder is deleted then local folder is renamed', async (done) => { + it('should handle conflict when remote folder is deleted then local folder is renamed', asyncTest(async () => { let folder1 = await Folder.save({ title: "folder1" }); let folder2 = await Folder.save({ title: "folder2" }); let note1 = await Note.save({ title: "un", parent_id: folder1.id }); @@ -457,11 +471,9 @@ describe('Synchronizer', function() { let items = await allItems(); expect(items.length).toBe(1); - - done(); - }); + })); - it('should allow duplicate folder titles', async (done) => { + it('should allow duplicate folder titles', asyncTest(async () => { let localF1 = await Folder.save({ title: "folder" }); await switchClient(2); @@ -493,11 +505,15 @@ describe('Synchronizer', function() { remoteF2 = await Folder.load(remoteF2.id); expect(remoteF2.title == localF2.title).toBe(true); + })); - done(); - }); + async function shoudSyncTagTest(withEncryption) { + let masterKey = null; + if (withEncryption) { + Setting.setValue('encryption.enabled', true); + masterKey = await loadEncryptionMasterKey(); + } - it('should sync tags', async (done) => { let f1 = await Folder.save({ title: "folder" }); let n1 = await Note.save({ title: "mynote" }); let n2 = await Note.save({ title: "mynote2" }); @@ -507,6 +523,12 @@ describe('Synchronizer', function() { await switchClient(2); await synchronizer().start(); + if (withEncryption) { + const masterKey_2 = await MasterKey.load(masterKey.id); + await encryptionService().loadMasterKey(masterKey_2, '123456', true); + let t = await Tag.load(tag.id); + await Tag.decrypt(t); + } let remoteTag = await Tag.loadByTitle(tag.title); expect(!!remoteTag).toBe(true); expect(remoteTag.id).toBe(tag.id); @@ -532,11 +554,15 @@ describe('Synchronizer', function() { noteIds = await Tag.noteIds(tag.id); expect(noteIds.length).toBe(1); expect(remoteNoteIds[0]).toBe(noteIds[0]); + } - done(); - }); + it('should sync tags', asyncTest(async () => { + await shoudSyncTagTest(false); })); - it('should not sync notes with conflicts', async (done) => { + it('should sync encrypted tags', asyncTest(async () => { + await shoudSyncTagTest(true); })); + + it('should not sync notes with conflicts', asyncTest(async () => { let f1 = await Folder.save({ title: "folder" }); let n1 = await Note.save({ title: "mynote", parent_id: f1.id, is_conflict: 1 }); await synchronizer().start(); @@ -548,11 +574,9 @@ describe('Synchronizer', function() { let folders = await Folder.all() expect(notes.length).toBe(0); expect(folders.length).toBe(1); + })); - done(); - }); - - it('should not try to delete on remote conflicted notes that have been deleted', async (done) => { + it('should not try to delete on remote conflicted notes that have been deleted', asyncTest(async () => { let f1 = await Folder.save({ title: "folder" }); let n1 = await Note.save({ title: "mynote", parent_id: f1.id }); await synchronizer().start(); @@ -565,17 +589,13 @@ describe('Synchronizer', function() { const deletedItems = await BaseItem.deletedItems(syncTargetId()); expect(deletedItems.length).toBe(0); - - done(); - }); - - it('should not consider it is a conflict if neither the title nor body of the note have changed', async (done) => { - // That was previously a common conflict: - // - Client 1 mark todo as "done", and sync - // - Client 2 doesn't sync, mark todo as "done" todo. Then sync. - // In theory it is a conflict because the todo_completed dates are different - // but in practice it doesn't matter, we can just take the date when the - // todo was marked as "done" the first time. + })); + + async function ignorableNoteConflictTest(withEncryption) { + if (withEncryption) { + Setting.setValue('encryption.enabled', true); + await loadEncryptionMasterKey(); + } let folder1 = await Folder.save({ title: "folder1" }); let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id }); @@ -584,6 +604,10 @@ describe('Synchronizer', function() { await switchClient(2); await synchronizer().start(); + if (withEncryption) { + await loadEncryptionMasterKey(null, true); + await decryptionWorker().start(); + } let note2 = await Note.load(note1.id); note2.todo_completed = time.unixMs()-1; await Note.save(note2); @@ -598,18 +622,43 @@ describe('Synchronizer', function() { note2conf = await Note.load(note1.id); await synchronizer().start(); - let conflictedNotes = await Note.conflictedNotes(); - expect(conflictedNotes.length).toBe(0); + if (!withEncryption) { + // That was previously a common conflict: + // - Client 1 mark todo as "done", and sync + // - Client 2 doesn't sync, mark todo as "done" todo. Then sync. + // In theory it is a conflict because the todo_completed dates are different + // but in practice it doesn't matter, we can just take the date when the + // todo was marked as "done" the first time. - let notes = await Note.all(); - expect(notes.length).toBe(1); - expect(notes[0].id).toBe(note1.id); - expect(notes[0].todo_completed).toBe(note2.todo_completed); + let conflictedNotes = await Note.conflictedNotes(); + expect(conflictedNotes.length).toBe(0); - done(); - }); + let notes = await Note.all(); + expect(notes.length).toBe(1); + expect(notes[0].id).toBe(note1.id); + expect(notes[0].todo_completed).toBe(note2.todo_completed); + } else { + // If the notes are encrypted however it's not possible to do this kind of + // smart conflict resolving since we don't know the content, so in that + // case it's handled as a regular conflict. - it('items should be downloaded again when user cancels in the middle of delta operation', async (done) => { + let conflictedNotes = await Note.conflictedNotes(); + expect(conflictedNotes.length).toBe(1); + + let notes = await Note.all(); + expect(notes.length).toBe(2); + } + } + + it('should not consider it is a conflict if neither the title nor body of the note have changed', asyncTest(async () => { + await ignorableNoteConflictTest(false); + })); + + it('should always handle conflict if local or remote are encrypted', asyncTest(async () => { + await ignorableNoteConflictTest(true); + })); + + it('items should be downloaded again when user cancels in the middle of delta operation', asyncTest(async () => { let folder1 = await Folder.save({ title: "folder1" }); let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id }); await synchronizer().start(); @@ -625,19 +674,17 @@ describe('Synchronizer', function() { await synchronizer().start({ context: context }); notes = await Note.all(); expect(notes.length).toBe(1); + })); - done(); - }); - - it('items should skip items that cannot be synced', async (done) => { + it('should skip items that cannot be synced', asyncTest(async () => { let folder1 = await Folder.save({ title: "folder1" }); let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id }); const noteId = note1.id; await synchronizer().start(); - let disabledItems = await BaseItem.syncDisabledItems(); + let disabledItems = await BaseItem.syncDisabledItems(syncTargetId()); expect(disabledItems.length).toBe(0); await Note.save({ id: noteId, title: "un mod", }); - synchronizer().debugFlags_ = ['cannotSync']; + synchronizer().debugFlags_ = ['rejectedByTarget']; await synchronizer().start(); synchronizer().debugFlags_ = []; await synchronizer().start(); // Another sync to check that this item is now excluded from sync @@ -651,10 +698,269 @@ describe('Synchronizer', function() { await switchClient(1); - disabledItems = await BaseItem.syncDisabledItems(); + disabledItems = await BaseItem.syncDisabledItems(syncTargetId()); expect(disabledItems.length).toBe(1); + })); - done(); - }); + it('notes and folders should get encrypted when encryption is enabled', asyncTest(async () => { + Setting.setValue('encryption.enabled', true); + const masterKey = await loadEncryptionMasterKey(); + let folder1 = await Folder.save({ title: "folder1" }); + let note1 = await Note.save({ title: "un", body: 'to be encrypted', parent_id: folder1.id }); + await synchronizer().start(); + // After synchronisation, remote items should be encrypted but local ones remain plain text + note1 = await Note.load(note1.id); + expect(note1.title).toBe('un'); + + await switchClient(2); + + await synchronizer().start(); + let folder1_2 = await Folder.load(folder1.id); + let note1_2 = await Note.load(note1.id); + let masterKey_2 = await MasterKey.load(masterKey.id); + // On this side however it should be received encrypted + expect(!note1_2.title).toBe(true); + expect(!folder1_2.title).toBe(true); + expect(!!note1_2.encryption_cipher_text).toBe(true); + expect(!!folder1_2.encryption_cipher_text).toBe(true); + // Master key is already encrypted so it does not get re-encrypted during sync + expect(masterKey_2.content).toBe(masterKey.content); + expect(masterKey_2.checksum).toBe(masterKey.checksum); + // Now load the master key we got from client 1 and try to decrypt + await encryptionService().loadMasterKey(masterKey_2, '123456', true); + // Get the decrypted items back + await Folder.decrypt(folder1_2); + await Note.decrypt(note1_2); + folder1_2 = await Folder.load(folder1.id); + note1_2 = await Note.load(note1.id); + // Check that properties match the original items. Also check + // the encryption did not affect the updated_time timestamp. + expect(note1_2.title).toBe(note1.title); + expect(note1_2.body).toBe(note1.body); + expect(note1_2.updated_time).toBe(note1.updated_time); + expect(!note1_2.encryption_cipher_text).toBe(true); + expect(folder1_2.title).toBe(folder1.title); + expect(folder1_2.updated_time).toBe(folder1.updated_time); + expect(!folder1_2.encryption_cipher_text).toBe(true); + })); + + it('should enable encryption automatically when downloading new master key (and none was previously available)',asyncTest(async () => { + // Enable encryption on client 1 and sync an item + Setting.setValue('encryption.enabled', true); + await loadEncryptionMasterKey(); + let folder1 = await Folder.save({ title: "folder1" }); + await synchronizer().start(); + + await switchClient(2); + + // Synchronising should enable encryption since we're going to get a master key + expect(Setting.value('encryption.enabled')).toBe(false); + await synchronizer().start(); + expect(Setting.value('encryption.enabled')).toBe(true); + + // Check that we got the master key from client 1 + const masterKey = (await MasterKey.all())[0]; + expect(!!masterKey).toBe(true); + + // Since client 2 hasn't supplied a password yet, no master key is currently loaded + expect(encryptionService().loadedMasterKeyIds().length).toBe(0); + + // If we sync now, nothing should be sent to target since we don't have a password. + // Technically it's incorrect to set the property of an encrypted variable but it allows confirming + // that encryption doesn't work if user hasn't supplied a password. + await BaseItem.forceSync(folder1.id); + await synchronizer().start(); + + await switchClient(1); + + await synchronizer().start(); + folder1 = await Folder.load(folder1.id); + expect(folder1.title).toBe('folder1'); // Still at old value + + await switchClient(2); + + // Now client 2 set the master key password + Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456'); + await encryptionService().loadMasterKeysFromSettings(); + + // Now that master key should be loaded + expect(encryptionService().loadedMasterKeyIds()[0]).toBe(masterKey.id); + + // Decrypt all the data. Now change the title and sync again - this time the changes should be transmitted + await decryptionWorker().start(); + folder1_2 = await Folder.save({ id: folder1.id, title: "change test" }); + + // If we sync now, this time client 1 should get the changes we did earlier + await synchronizer().start(); + + await switchClient(1); + + await synchronizer().start(); + // Decrypt the data we just got + await decryptionWorker().start(); + folder1 = await Folder.load(folder1.id); + expect(folder1.title).toBe('change test'); // Got title from client 2 + })); + + it('should encrypt existing notes too when enabling E2EE', asyncTest(async () => { + // First create a folder, without encryption enabled, and sync it + let folder1 = await Folder.save({ title: "folder1" }); + await synchronizer().start(); + let files = await fileApi().list() + let content = await fileApi().get(files.items[0].path); + expect(content.indexOf('folder1') >= 0).toBe(true) + + // Then enable encryption and sync again + let masterKey = await encryptionService().generateMasterKey('123456'); + masterKey = await MasterKey.save(masterKey); + await encryptionService().enableEncryption(masterKey, '123456'); + await encryptionService().loadMasterKeysFromSettings(); + await synchronizer().start(); + + // Even though the folder has not been changed it should have been synced again so that + // an encrypted version of it replaces the decrypted version. + files = await fileApi().list() + expect(files.items.length).toBe(2); + // By checking that the folder title is not present, we can confirm that the item has indeed been encrypted + // One of the two items is the master key + content = await fileApi().get(files.items[0].path); + expect(content.indexOf('folder1') < 0).toBe(true); + content = await fileApi().get(files.items[1].path); + expect(content.indexOf('folder1') < 0).toBe(true); + })); + + it('should sync resources', asyncTest(async () => { + let folder1 = await Folder.save({ title: "folder1" }); + let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); + await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); + let resource1 = (await Resource.all())[0]; + let resourcePath1 = Resource.fullPath(resource1); + await synchronizer().start(); + expect((await fileApi().list()).items.length).toBe(3); + + await switchClient(2); + + await synchronizer().start(); + let allResources = await Resource.all(); + expect(allResources.length).toBe(1); + let resource1_2 = allResources[0]; + let resourcePath1_2 = Resource.fullPath(resource1_2); + + expect(resource1_2.id).toBe(resource1.id); + expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true); + })); + + it('should encryt resources', asyncTest(async () => { + Setting.setValue('encryption.enabled', true); + const masterKey = await loadEncryptionMasterKey(); + + let folder1 = await Folder.save({ title: "folder1" }); + let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); + await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); + let resource1 = (await Resource.all())[0]; + let resourcePath1 = Resource.fullPath(resource1); + await synchronizer().start(); + + await switchClient(2); + + await synchronizer().start(); + Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456'); + await encryptionService().loadMasterKeysFromSettings(); + + let resource1_2 = (await Resource.all())[0]; + resource1_2 = await Resource.decrypt(resource1_2); + let resourcePath1_2 = Resource.fullPath(resource1_2); + + expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true); + })); + + it('should upload decrypted items to sync target after encryption disabled', asyncTest(async () => { + Setting.setValue('encryption.enabled', true); + const masterKey = await loadEncryptionMasterKey(); + + let folder1 = await Folder.save({ title: "folder1" }); + await synchronizer().start(); + + let allEncrypted = await allSyncTargetItemsEncrypted(); + expect(allEncrypted).toBe(true); + + await encryptionService().disableEncryption(); + + await synchronizer().start(); + allEncrypted = await allSyncTargetItemsEncrypted(); + expect(allEncrypted).toBe(false); + })); + + it('should not upload any item if encryption was enabled, and items have not been decrypted, and then encryption disabled', asyncTest(async () => { + // For some reason I can't explain, this test is sometimes executed before beforeEach is finished + // which means it's going to fail in unexpected way. So the loop below wait for beforeEach to be done. + while (insideBeforeEach) await time.msleep(100); + + Setting.setValue('encryption.enabled', true); + const masterKey = await loadEncryptionMasterKey(); + + let folder1 = await Folder.save({ title: "folder1" }); + await synchronizer().start(); + + await switchClient(2); + + await synchronizer().start(); + expect(Setting.value('encryption.enabled')).toBe(true); + + // If we try to disable encryption now, it should throw an error because some items are + // currently encrypted. They must be decrypted first so that they can be sent as + // plain text to the sync target. + //let hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption()); + //expect(hasThrown).toBe(true); + + // Now supply the password, and decrypt the items + Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456'); + await encryptionService().loadMasterKeysFromSettings(); + await decryptionWorker().start(); + + // Try to disable encryption again + hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption()); + expect(hasThrown).toBe(false); + + // If we sync now the target should receive the decrypted items + await synchronizer().start(); + allEncrypted = await allSyncTargetItemsEncrypted(); + expect(allEncrypted).toBe(false); + })); + + it('should encrypt remote resources after encryption has been enabled', asyncTest(async () => { + while (insideBeforeEach) await time.msleep(100); + + let folder1 = await Folder.save({ title: "folder1" }); + let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); + await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); + let resource1 = (await Resource.all())[0]; + await synchronizer().start(); + + expect(await allSyncTargetItemsEncrypted()).toBe(false); + + const masterKey = await loadEncryptionMasterKey(); + await encryptionService().enableEncryption(masterKey, '123456'); + await encryptionService().loadMasterKeysFromSettings(); + + await synchronizer().start(); + + expect(await allSyncTargetItemsEncrypted()).toBe(true); + })); + + it('should upload encrypted resource, but it should not mark the blob as encrypted locally', asyncTest(async () => { + while (insideBeforeEach) await time.msleep(100); + + let folder1 = await Folder.save({ title: "folder1" }); + let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); + await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); + const masterKey = await loadEncryptionMasterKey(); + await encryptionService().enableEncryption(masterKey, '123456'); + await encryptionService().loadMasterKeysFromSettings(); + await synchronizer().start(); + + let resource1 = (await Resource.all())[0]; + expect(resource1.encryption_blob_encrypted).toBe(0); + })); }); \ No newline at end of file diff --git a/CliClient/tests/test-utils.js b/CliClient/tests/test-utils.js index fa02845f9..84b49ed9c 100644 --- a/CliClient/tests/test-utils.js +++ b/CliClient/tests/test-utils.js @@ -1,34 +1,43 @@ const fs = require('fs-extra'); const { JoplinDatabase } = require('lib/joplin-database.js'); const { DatabaseDriverNode } = require('lib/database-driver-node.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); -const { Resource } = require('lib/models/resource.js'); -const { Tag } = require('lib/models/tag.js'); -const { NoteTag } = require('lib/models/note-tag.js'); +const BaseModel = require('lib/BaseModel.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); +const Resource = require('lib/models/Resource.js'); +const Tag = require('lib/models/Tag.js'); +const NoteTag = require('lib/models/NoteTag.js'); const { Logger } = require('lib/logger.js'); -const { Setting } = require('lib/models/setting.js'); -const { BaseItem } = require('lib/models/base-item.js'); +const Setting = require('lib/models/Setting.js'); +const MasterKey = require('lib/models/MasterKey'); +const BaseItem = require('lib/models/BaseItem.js'); const { Synchronizer } = require('lib/synchronizer.js'); const { FileApi } = require('lib/file-api.js'); const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js'); const { FileApiDriverLocal } = require('lib/file-api-driver-local.js'); const { FsDriverNode } = require('lib/fs-driver-node.js'); const { time } = require('lib/time-utils.js'); +const { shimInit } = require('lib/shim-init-node.js'); const SyncTargetRegistry = require('lib/SyncTargetRegistry.js'); const SyncTargetMemory = require('lib/SyncTargetMemory.js'); const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js'); const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js'); +const EncryptionService = require('lib/services/EncryptionService.js'); +const DecryptionWorker = require('lib/services/DecryptionWorker.js'); let databases_ = []; let synchronizers_ = []; +let encryptionServices_ = []; +let decryptionWorkers_ = []; let fileApi_ = null; let currentClient_ = 1; +shimInit(); + const fsDriver = new FsDriverNode(); Logger.fsDriver_ = fsDriver; Resource.fsDriver_ = fsDriver; +EncryptionService.fsDriver_ = fsDriver; const logDir = __dirname + '/../tests/logs'; fs.mkdirpSync(logDir, 0o755); @@ -37,7 +46,8 @@ SyncTargetRegistry.addClass(SyncTargetMemory); SyncTargetRegistry.addClass(SyncTargetFilesystem); SyncTargetRegistry.addClass(SyncTargetOneDrive); -const syncTargetId_ = SyncTargetRegistry.nameToId('memory'); +//const syncTargetId_ = SyncTargetRegistry.nameToId('memory'); +const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem'); const syncDir = __dirname + '/../tests/sync'; const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 400; @@ -45,13 +55,14 @@ const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1 const logger = new Logger(); logger.addTarget('console'); logger.addTarget('file', { path: logDir + '/log.txt' }); -logger.setLevel(Logger.LEVEL_WARN); +logger.setLevel(Logger.LEVEL_WARN); // Set to INFO to display sync process in console BaseItem.loadClass('Note', Note); BaseItem.loadClass('Folder', Folder); BaseItem.loadClass('Resource', Resource); BaseItem.loadClass('Tag', Tag); BaseItem.loadClass('NoteTag', NoteTag); +BaseItem.loadClass('MasterKey', MasterKey); Setting.setConstant('appId', 'net.cozic.joplin-cli'); Setting.setConstant('appType', 'cli'); @@ -79,10 +90,15 @@ async function switchClient(id) { BaseItem.db_ = databases_[id]; Setting.db_ = databases_[id]; + BaseItem.encryptionService_ = encryptionServices_[id]; + Resource.encryptionService_ = encryptionServices_[id]; + + Setting.setConstant('resourceDir', resourceDir(id)); + return Setting.load(); } -function clearDatabase(id = null) { +async function clearDatabase(id = null) { if (id === null) id = currentClient_; let queries = [ @@ -91,35 +107,65 @@ function clearDatabase(id = null) { 'DELETE FROM resources', 'DELETE FROM tags', 'DELETE FROM note_tags', + 'DELETE FROM master_keys', + 'DELETE FROM settings', 'DELETE FROM deleted_items', 'DELETE FROM sync_items', ]; - return databases_[id].transactionExecBatch(queries); + await databases_[id].transactionExecBatch(queries); } -function setupDatabase(id = null) { +async function setupDatabase(id = null) { if (id === null) id = currentClient_; + Setting.cancelScheduleSave(); + Setting.cache_ = null; + if (databases_[id]) { - return clearDatabase(id).then(() => { - return Setting.load(); - }); + await clearDatabase(id); + await Setting.load(); + return; } const filePath = __dirname + '/data/test-' + id + '.sqlite'; - return fs.unlink(filePath).catch(() => { + + try { + await fs.unlink(filePath); + } catch (error) { // Don't care if the file doesn't exist - }).then(() => { - databases_[id] = new JoplinDatabase(new DatabaseDriverNode()); - // databases_[id].setLogger(logger); - // console.info(filePath); - return databases_[id].open({ name: filePath }).then(() => { - BaseModel.db_ = databases_[id]; - return setupDatabase(id); - }); - }); + }; + + databases_[id] = new JoplinDatabase(new DatabaseDriverNode()); + await databases_[id].open({ name: filePath }); + + BaseModel.db_ = databases_[id]; + await Setting.load(); + //return setupDatabase(id); + + + + // return databases_[id].open({ name: filePath }).then(() => { + // BaseModel.db_ = databases_[id]; + // return setupDatabase(id); + // }); + + + // return fs.unlink(filePath).catch(() => { + // // Don't care if the file doesn't exist + // }).then(() => { + // databases_[id] = new JoplinDatabase(new DatabaseDriverNode()); + // return databases_[id].open({ name: filePath }).then(() => { + // BaseModel.db_ = databases_[id]; + // return setupDatabase(id); + // }); + // }); +} + +function resourceDir(id = null) { + if (id === null) id = currentClient_; + return __dirname + '/data/resources-' + id; } async function setupDatabaseAndSynchronizer(id = null) { @@ -127,14 +173,25 @@ async function setupDatabaseAndSynchronizer(id = null) { await setupDatabase(id); + EncryptionService.instance_ = null; + DecryptionWorker.instance_ = null; + + await fs.remove(resourceDir(id)); + await fs.mkdirp(resourceDir(id), 0o755); + if (!synchronizers_[id]) { const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_); const syncTarget = new SyncTargetClass(db(id)); syncTarget.setFileApi(fileApi()); syncTarget.setLogger(logger); synchronizers_[id] = await syncTarget.synchronizer(); + synchronizers_[id].autoStartDecryptionWorker_ = false; // For testing we disable this since it would make the tests non-deterministic } + encryptionServices_[id] = new EncryptionService(); + decryptionWorkers_[id] = new DecryptionWorker(); + decryptionWorkers_[id].setEncryptionService(encryptionServices_[id]); + if (syncTargetId_ == SyncTargetRegistry.nameToId('filesystem')) { fs.removeSync(syncDir) fs.mkdirpSync(syncDir, 0o755); @@ -153,6 +210,35 @@ function synchronizer(id = null) { return synchronizers_[id]; } +function encryptionService(id = null) { + if (id === null) id = currentClient_; + return encryptionServices_[id]; +} + +function decryptionWorker(id = null) { + if (id === null) id = currentClient_; + return decryptionWorkers_[id]; +} + +async function loadEncryptionMasterKey(id = null, useExisting = false) { + const service = encryptionService(id); + + let masterKey = null; + + if (!useExisting) { // Create it + masterKey = await service.generateMasterKey('123456'); + masterKey = await MasterKey.save(masterKey); + } else { // Use the one already available + materKey = await MasterKey.all(); + if (!materKey.length) throw new Error('No mater key available'); + masterKey = materKey[0]; + } + + await service.loadMasterKey(masterKey, '123456', true); + + return masterKey; +} + function fileApi() { if (fileApi_) return fileApi_; @@ -185,4 +271,43 @@ function fileApi() { return fileApi_; } -module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId }; \ No newline at end of file +function objectsEqual(o1, o2) { + if (Object.getOwnPropertyNames(o1).length !== Object.getOwnPropertyNames(o2).length) return false; + for (let n in o1) { + if (!o1.hasOwnProperty(n)) continue; + if (o1[n] !== o2[n]) return false; + } + return true; +} + +async function checkThrowAsync(asyncFn) { + let hasThrown = false; + try { + await asyncFn(); + } catch (error) { + hasThrown = true; + } + return hasThrown; +} + +function fileContentEqual(path1, path2) { + const fs = require('fs-extra'); + const content1 = fs.readFileSync(path1, 'base64'); + const content2 = fs.readFileSync(path2, 'base64'); + return content1 === content2; +} + +// Wrap an async test in a try/catch block so that done() is always called +// and display a proper error message instead of "unhandled promise error" +function asyncTest(callback) { + return async function(done) { + try { + await callback(); + } catch (error) { + console.error(error); + } + done(); + } +} + +module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest }; \ No newline at end of file diff --git a/CliClientDemo/package.json b/CliClientDemo/package.json index 136a2687e..e0dbdf8d2 100644 --- a/CliClientDemo/package.json +++ b/CliClientDemo/package.json @@ -19,7 +19,8 @@ "title": "Demo for Joplin CLI", "years": [ 2016, - 2017 + 2017, + 2018 ], "owner": "Laurent Cozic" }, diff --git a/ElectronClient/app/app.js b/ElectronClient/app/app.js index a951f2c0f..3dbedb922 100644 --- a/ElectronClient/app/app.js +++ b/ElectronClient/app/app.js @@ -2,13 +2,14 @@ require('app-module-path').addPath(__dirname); const { BaseApplication } = require('lib/BaseApplication'); const { FoldersScreenUtils } = require('lib/folders-screen-utils.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { shim } = require('lib/shim.js'); -const { BaseModel } = require('lib/base-model.js'); +const BaseModel = require('lib/BaseModel.js'); +const MasterKey = require('lib/models/MasterKey'); const { _, setLocale } = require('lib/locale.js'); const os = require('os'); const fs = require('fs-extra'); -const { Tag } = require('lib/models/tag.js'); +const Tag = require('lib/models/Tag.js'); const { reg } = require('lib/registry.js'); const { sprintf } = require('sprintf-js'); const { JoplinDatabase } = require('lib/joplin-database.js'); @@ -18,6 +19,7 @@ const { defaultState } = require('lib/reducer.js'); const packageInfo = require('./packageInfo.js'); const AlarmService = require('lib/services/AlarmService.js'); const AlarmServiceDriverNode = require('lib/services/AlarmServiceDriverNode'); +const DecryptionWorker = require('lib/services/DecryptionWorker'); const { bridge } = require('electron').remote.require('./bridge'); const Menu = bridge().Menu; @@ -255,7 +257,7 @@ class Application extends BaseApplication { name: 'search', }); }, - }] + }], }, { label: _('Tools'), submenu: [{ @@ -266,15 +268,26 @@ class Application extends BaseApplication { routeName: 'Status', }); } + }, { + type: 'separator', + screens: ['Main'], },{ - label: _('Options'), + label: _('Encryption options'), + click: () => { + this.dispatch({ + type: 'NAV_GO', + routeName: 'EncryptionConfig', + }); + } + },{ + label: _('General Options'), click: () => { this.dispatch({ type: 'NAV_GO', routeName: 'Config', }); } - }] + }], }, { label: _('Help'), submenu: [{ @@ -288,7 +301,7 @@ class Application extends BaseApplication { let message = [ p.description, '', - 'Copyright © 2016-2017 Laurent Cozic', + 'Copyright © 2016-2018 Laurent Cozic', _('%s %s (%s, %s)', p.name, p.version, Setting.value('env'), process.platform), ]; bridge().showMessageBox({ @@ -354,7 +367,14 @@ class Application extends BaseApplication { this.dispatch({ type: 'TAG_UPDATE_ALL', - tags: tags, + items: tags, + }); + + const masterKeys = await MasterKey.all(); + + this.dispatch({ + type: 'MASTERKEY_UPDATE_ALL', + items: masterKeys, }); this.store().dispatch({ @@ -387,6 +407,8 @@ class Application extends BaseApplication { // Wait for the first sync before updating the notifications, since synchronisation // might change the notifications. AlarmService.updateAllNotifications(); + + DecryptionWorker.instance().scheduleStart(); }); } } diff --git a/ElectronClient/app/gui/ConfigScreen.jsx b/ElectronClient/app/gui/ConfigScreen.jsx index b2a5c4dac..ed72bd11f 100644 --- a/ElectronClient/app/gui/ConfigScreen.jsx +++ b/ElectronClient/app/gui/ConfigScreen.jsx @@ -1,7 +1,7 @@ const React = require('react'); const { connect } = require('react-redux'); const { reg } = require('lib/registry.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { bridge } = require('electron').remote.require('./bridge'); const { Header } = require('./Header.min.js'); const { themeStyle } = require('../theme.js'); @@ -22,6 +22,23 @@ class ConfigScreenComponent extends React.Component { this.setState({ settings: this.props.settings }); } + keyValueToArray(kv) { + let output = []; + for (let k in kv) { + if (!kv.hasOwnProperty(k)) continue; + output.push({ + key: k, + label: kv[k], + }); + } + + output.sort((a, b) => { + return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : +1; + }); + + return output; + } + settingToComponent(key, value) { const theme = themeStyle(this.props.theme); @@ -53,9 +70,10 @@ class ConfigScreenComponent extends React.Component { if (md.isEnum) { let items = []; const settingOptions = md.options(); - for (let k in settingOptions) { - if (!settingOptions.hasOwnProperty(k)) continue; - items.push(); + let array = this.keyValueToArray(settingOptions); + for (let i = 0; i < array.length; i++) { + const e = array[i]; + items.push(); } return ( diff --git a/ElectronClient/app/gui/EncryptionConfigScreen.jsx b/ElectronClient/app/gui/EncryptionConfigScreen.jsx new file mode 100644 index 000000000..6512d9c17 --- /dev/null +++ b/ElectronClient/app/gui/EncryptionConfigScreen.jsx @@ -0,0 +1,191 @@ +const React = require('react'); +const { connect } = require('react-redux'); +const Setting = require('lib/models/Setting'); +const BaseItem = require('lib/models/BaseItem'); +const EncryptionService = require('lib/services/EncryptionService'); +const { Header } = require('./Header.min.js'); +const { themeStyle } = require('../theme.js'); +const { _ } = require('lib/locale.js'); +const { time } = require('lib/time-utils.js'); +const dialogs = require('./dialogs'); +const shared = require('lib/components/shared/encryption-config-shared.js'); +const pathUtils = require('lib/path-utils.js'); +const { bridge } = require('electron').remote.require('./bridge'); + +class EncryptionConfigScreenComponent extends React.Component { + + constructor() { + super(); + shared.constructor(this); + } + + componentDidMount() { + this.isMounted_ = true; + } + + componentWillUnmount() { + this.isMounted_ = false; + } + + initState(props) { + return shared.initState(this, props); + } + + async refreshStats() { + return shared.refreshStats(this); + } + + componentWillMount() { + this.initState(this.props); + } + + componentWillReceiveProps(nextProps) { + this.initState(nextProps); + } + + async checkPasswords() { + return shared.checkPasswords(this); + } + + renderMasterKey(mk) { + const theme = themeStyle(this.props.theme); + + const onSaveClick = () => { + return shared.onSavePasswordClick(this, mk); + } + + const onPasswordChange = (event) => { + return shared.onPasswordChange(this, mk, event.target.value); + } + + const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : ''; + const active = this.props.activeMasterKeyId === mk.id ? '✔' : ''; + const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌'; + + return ( + + {active} + {mk.id} + {mk.source_application} + {time.formatMsToLocal(mk.created_time)} + {time.formatMsToLocal(mk.updated_time)} + onPasswordChange(event)}/> + {passwordOk} + + ); + } + + render() { + const style = this.props.style; + const theme = themeStyle(this.props.theme); + const masterKeys = this.state.masterKeys; + const containerPadding = 10; + + const headerStyle = { + width: style.width, + }; + + const containerStyle = { + padding: containerPadding, + overflow: 'auto', + height: style.height - theme.headerHeight - containerPadding * 2, + }; + + const mkComps = []; + + for (let i = 0; i < masterKeys.length; i++) { + const mk = masterKeys[i]; + mkComps.push(this.renderMasterKey(mk)); + } + + const onToggleButtonClick = async () => { + const isEnabled = Setting.value('encryption.enabled'); + + let answer = null; + if (isEnabled) { + answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?')); + } else { + answer = await dialogs.prompt(_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.'), '', '', { type: 'password' }); + } + + if (!answer) return; + + try { + if (isEnabled) { + await EncryptionService.instance().disableEncryption(); + } else { + await EncryptionService.instance().generateMasterKeyAndEnableEncryption(answer); + } + } catch (error) { + await dialogs.alert(error.message); + } + } + + const decryptedItemsInfo = this.props.encryptionEnabled ?

{shared.decryptedStatText(this)}

: null; + const toggleButton = + + let masterKeySection = null; + + if (mkComps.length) { + masterKeySection = ( +
+

{_('Master Keys')}

+ + + + + + + + + + + + {mkComps} + +
{_('Active')}{_('ID')}{_('Source')}{_('Created')}{_('Updated')}{_('Password')}{_('Password OK')}
+

{_('Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.')}

+
+ ); + } + + return ( +
+
+
+
+

+ Important: This is a beta feature. It has been extensively tested and is already in use by some users, but it is possible that some bugs remain. +

+

+ If you wish to you use it, it is recommended that you keep a backup of your data. The simplest way is to regularly backup {pathUtils.toSystemSlashes(Setting.value('profileDir'), process.platform)} +

+

+ For more information about End-To-End Encryption (E2EE) and how it is going to work, please check the documentation: {bridge().openExternal('http://joplin.cozic.net/help/e2ee.html')}} href="#">http://joplin.cozic.net/help/e2ee.html +

+
+

{_('Status')}

+

{_('Encryption is:')} {this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}

+ {decryptedItemsInfo} + {toggleButton} + {masterKeySection} +
+
+ ); + } + +} + +const mapStateToProps = (state) => { + return { + theme: state.settings.theme, + masterKeys: state.masterKeys, + passwords: state.settings['encryption.passwordCache'], + encryptionEnabled: state.settings['encryption.enabled'], + activeMasterKeyId: state.settings['encryption.activeMasterKeyId'], + }; +}; + +const EncryptionConfigScreen = connect(mapStateToProps)(EncryptionConfigScreenComponent); + +module.exports = { EncryptionConfigScreen }; \ No newline at end of file diff --git a/ElectronClient/app/gui/ImportScreen.jsx b/ElectronClient/app/gui/ImportScreen.jsx index 4cb82bd44..573b4c5a0 100644 --- a/ElectronClient/app/gui/ImportScreen.jsx +++ b/ElectronClient/app/gui/ImportScreen.jsx @@ -1,7 +1,7 @@ const React = require('react'); const { connect } = require('react-redux'); const { reg } = require('lib/registry.js'); -const { Folder } = require('lib/models/folder.js'); +const Folder = require('lib/models/Folder.js'); const { bridge } = require('electron').remote.require('./bridge'); const { Header } = require('./Header.min.js'); const { themeStyle } = require('../theme.js'); diff --git a/ElectronClient/app/gui/MainScreen.jsx b/ElectronClient/app/gui/MainScreen.jsx index 56efddfd4..742d7f0b8 100644 --- a/ElectronClient/app/gui/MainScreen.jsx +++ b/ElectronClient/app/gui/MainScreen.jsx @@ -5,12 +5,12 @@ const { SideBar } = require('./SideBar.min.js'); const { NoteList } = require('./NoteList.min.js'); const { NoteText } = require('./NoteText.min.js'); const { PromptDialog } = require('./PromptDialog.min.js'); -const { Setting } = require('lib/models/setting.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Tag } = require('lib/models/tag.js'); -const { Note } = require('lib/models/note.js'); +const Setting = require('lib/models/Setting.js'); +const BaseModel = require('lib/BaseModel.js'); +const Tag = require('lib/models/Tag.js'); +const Note = require('lib/models/Note.js'); const { uuid } = require('lib/uuid.js'); -const { Folder } = require('lib/models/folder.js'); +const Folder = require('lib/models/Folder.js'); const { themeStyle } = require('../theme.js'); const { _ } = require('lib/locale.js'); const layoutUtils = require('lib/layout-utils.js'); @@ -132,7 +132,7 @@ class MainScreenComponent extends React.Component { } }, }); - } else if (command.name === 'renameNotebook') { + } else if (command.name === 'renameFolder') { const folder = await Folder.load(command.id); if (!folder) return; @@ -143,7 +143,8 @@ class MainScreenComponent extends React.Component { onClose: async (answer) => { if (answer !== null) { try { - await Folder.save({ id: folder.id, title: answer }, { userSideValidation: true }); + folder.title = answer; + await Folder.save(folder, { fields: ['title'], userSideValidation: true }); } catch (error) { bridge().showErrorMessageBox(error.message); } @@ -288,8 +289,7 @@ class MainScreenComponent extends React.Component { const promptOptions = this.state.promptOptions; const folders = this.props.folders; const notes = this.props.notes; - const messageBoxVisible = this.props.hasDisabledSyncItems; - + const messageBoxVisible = this.props.hasDisabledSyncItems || this.props.showMissingMasterKeyMessage; const styles = this.styles(this.props.theme, style.width, style.height, messageBoxVisible); const theme = themeStyle(this.props.theme); @@ -343,13 +343,31 @@ class MainScreenComponent extends React.Component { }); } - const messageComp = messageBoxVisible ? ( -
- - {_('Some items cannot be synchronised.')} { onViewDisabledItemsClick() }}>{_('View them now')} - -
- ) : null; + const onViewMasterKeysClick = () => { + this.props.dispatch({ + type: 'NAV_GO', + routeName: 'EncryptionConfig', + }); + } + + let messageComp = null; + + if (messageBoxVisible) { + let msg = null; + if (this.props.hasDisabledSyncItems) { + msg = {_('Some items cannot be synchronised.')} { onViewDisabledItemsClick() }}>{_('View them now')} + } else if (this.props.showMissingMasterKeyMessage) { + msg = {_('Some items cannot be decrypted.')} { onViewMasterKeysClick() }}>{_('Set the password')} + } + + messageComp = ( +
+ + {msg} + +
+ ); + } return (
@@ -383,6 +401,7 @@ const mapStateToProps = (state) => { folders: state.folders, notes: state.notes, hasDisabledSyncItems: state.hasDisabledSyncItems, + showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length, }; }; diff --git a/ElectronClient/app/gui/NoteList.jsx b/ElectronClient/app/gui/NoteList.jsx index ef5138c61..bc4c49284 100644 --- a/ElectronClient/app/gui/NoteList.jsx +++ b/ElectronClient/app/gui/NoteList.jsx @@ -3,6 +3,7 @@ const React = require('react'); const { connect } = require('react-redux'); const { time } = require('lib/time-utils.js'); const { themeStyle } = require('../theme.js'); +const BaseModel = require('lib/BaseModel'); const { _ } = require('lib/locale.js'); const { bridge } = require('electron').remote.require('./bridge'); const Menu = bridge().Menu; @@ -56,23 +57,32 @@ class NoteListComponent extends React.Component { const noteIds = this.props.selectedNoteIds; if (!noteIds.length) return; + const notes = noteIds.map((id) => BaseModel.byId(this.props.notes, id)); + + let hasEncrypted = false; + for (let i = 0; i < notes.length; i++) { + if (!!notes[i].encryption_applied) hasEncrypted = true; + } + const menu = new Menu() - menu.append(new MenuItem({label: _('Add or remove tags'), enabled: noteIds.length === 1, click: async () => { - this.props.dispatch({ - type: 'WINDOW_COMMAND', - name: 'setTags', - noteId: noteIds[0], - }); - }})); + if (!hasEncrypted) { + menu.append(new MenuItem({label: _('Add or remove tags'), enabled: noteIds.length === 1, click: async () => { + this.props.dispatch({ + type: 'WINDOW_COMMAND', + name: 'setTags', + noteId: noteIds[0], + }); + }})); - menu.append(new MenuItem({label: _('Switch between note and to-do type'), click: async () => { - for (let i = 0; i < noteIds.length; i++) { - const note = await Note.load(noteIds[i]); - await Note.save(Note.toggleIsTodo(note)); - eventManager.emit('noteTypeToggle', { noteId: note.id }); - } - }})); + menu.append(new MenuItem({label: _('Switch between note and to-do type'), click: async () => { + for (let i = 0; i < noteIds.length; i++) { + const note = await Note.load(noteIds[i]); + await Note.save(Note.toggleIsTodo(note), { userSideValidation: true }); + eventManager.emit('noteTypeToggle', { noteId: note.id }); + } + }})); + } menu.append(new MenuItem({label: _('Delete'), click: async () => { const ok = bridge().showConfirmMessageBox(noteIds.length > 1 ? _('Delete notes?') : _('Delete note?')); @@ -120,7 +130,7 @@ class NoteListComponent extends React.Component { id: item.id, todo_completed: checked ? time.unixMs() : 0, } - await Note.save(newNote); + await Note.save(newNote, { userSideValidation: true }); eventManager.emit('todoToggle', { noteId: item.id }); } @@ -154,7 +164,7 @@ class NoteListComponent extends React.Component { onClick={(event) => { onTitleClick(event, item) }} onDragStart={(event) => onDragStart(event) } > - {item.title} + {Note.displayTitle(item)}
} diff --git a/ElectronClient/app/gui/NoteText.jsx b/ElectronClient/app/gui/NoteText.jsx index 885f487ed..d218d1933 100644 --- a/ElectronClient/app/gui/NoteText.jsx +++ b/ElectronClient/app/gui/NoteText.jsx @@ -1,7 +1,7 @@ const React = require('react'); -const { Note } = require('lib/models/note.js'); +const Note = require('lib/models/Note.js'); const { time } = require('lib/time-utils.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { IconButton } = require('./IconButton.min.js'); const Toolbar = require('./Toolbar.min.js'); const { connect } = require('react-redux'); @@ -36,7 +36,7 @@ class NoteTextComponent extends React.Component { isLoading: true, webviewReady: false, scrollHeight: null, - editorScrollTop: 0, + editorScrollTop: 0 }; this.lastLoadedNoteId_ = null; @@ -167,6 +167,12 @@ class NoteTextComponent extends React.Component { async componentWillReceiveProps(nextProps) { if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) { await this.reloadNote(nextProps); + if(this.editor_){ + const session = this.editor_.editor.getSession(); + const undoManager = session.getUndoManager(); + undoManager.reset(); + session.setUndoManager(undoManager); + } } if ('syncStarted' in nextProps && !nextProps.syncStarted && !this.isModified()) { @@ -334,6 +340,7 @@ class NoteTextComponent extends React.Component { this.scheduleSave(); } + async commandAttachFile() { const noteId = this.props.noteId; if (!noteId) return; @@ -418,7 +425,7 @@ class NoteTextComponent extends React.Component { const innerWidth = rootStyle.width - rootStyle.paddingLeft - rootStyle.paddingRight - borderWidth; - if (!note) { + if (!note || !!note.encryption_applied) { const emptyDivStyle = Object.assign({ backgroundColor: 'black', opacity: 0.1, @@ -549,7 +556,6 @@ class NoteTextComponent extends React.Component { delete editorRootStyle.width; delete editorRootStyle.height; delete editorRootStyle.fontSize; - const editor = _('Import') }, Config: { screen: ConfigScreen, title: () => _('Options') }, Status: { screen: StatusScreen, title: () => _('Synchronisation Status') }, + EncryptionConfig: { screen: EncryptionConfigScreen, title: () => _('Encryption Options') }, }; return ( diff --git a/ElectronClient/app/gui/SideBar.jsx b/ElectronClient/app/gui/SideBar.jsx index 846ef890d..a977e54ed 100644 --- a/ElectronClient/app/gui/SideBar.jsx +++ b/ElectronClient/app/gui/SideBar.jsx @@ -2,10 +2,10 @@ const React = require('react'); const { connect } = require('react-redux'); const shared = require('lib/components/shared/side-menu-shared.js'); const { Synchronizer } = require('lib/synchronizer.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); -const { Tag } = require('lib/models/tag.js'); +const BaseModel = require('lib/BaseModel.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); +const Tag = require('lib/models/Tag.js'); const { _ } = require('lib/locale.js'); const { themeStyle } = require('../theme.js'); const { bridge } = require('electron').remote.require('./bridge'); @@ -107,6 +107,11 @@ class SideBarComponent extends React.Component { const menu = new Menu(); + let item = null; + if (itemType === BaseModel.TYPE_FOLDER) { + item = BaseModel.byId(this.props.folders, itemId); + } + menu.append(new MenuItem({label: _('Delete'), click: async () => { const ok = bridge().showConfirmMessageBox(deleteMessage); if (!ok) return; @@ -123,11 +128,11 @@ class SideBarComponent extends React.Component { } }})) - if (itemType === BaseModel.TYPE_FOLDER) { + if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) { menu.append(new MenuItem({label: _('Rename'), click: async () => { this.props.dispatch({ type: 'WINDOW_COMMAND', - name: 'renameNotebook', + name: 'renameFolder', id: itemId, }); }})) @@ -180,6 +185,8 @@ class SideBarComponent extends React.Component { } } + const itemTitle = Folder.displayTitle(folder); + return { onDragOver(event, folder) } } @@ -189,14 +196,14 @@ class SideBarComponent extends React.Component { data-type={BaseModel.TYPE_FOLDER} onContextMenu={(event) => this.itemContextMenu(event)} key={folder.id} - style={style} onClick={() => {this.folderItem_click(folder)}}>{folder.title} + style={style} onClick={() => {this.folderItem_click(folder)}}>{itemTitle} } tagItem(tag, selected) { let style = Object.assign({}, this.style().listItem); if (selected) style = Object.assign(style, this.style().listItemSelected); - return this.itemContextMenu(event)} key={tag.id} style={style} onClick={() => {this.tagItem_click(tag)}}>{tag.title} + return this.itemContextMenu(event)} key={tag.id} style={style} onClick={() => {this.tagItem_click(tag)}}>{Tag.displayTitle(tag)} } searchItem(search, selected) { diff --git a/ElectronClient/app/gui/StatusScreen.jsx b/ElectronClient/app/gui/StatusScreen.jsx index 5717ac280..1258220de 100644 --- a/ElectronClient/app/gui/StatusScreen.jsx +++ b/ElectronClient/app/gui/StatusScreen.jsx @@ -1,7 +1,7 @@ const React = require('react'); const { connect } = require('react-redux'); const { reg } = require('lib/registry.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { bridge } = require('electron').remote.require('./bridge'); const { Header } = require('./Header.min.js'); const { themeStyle } = require('../theme.js'); @@ -70,7 +70,9 @@ class StatusScreenComponent extends React.Component { for (let n in section.body) { if (!section.body.hasOwnProperty(n)) continue; - itemsHtml.push(
{section.body[n]}
); + let text = section.body[n]; + if (!text) text = '\xa0'; + itemsHtml.push(
{text}
); } return ( diff --git a/ElectronClient/app/gui/dialogs.js b/ElectronClient/app/gui/dialogs.js new file mode 100644 index 000000000..e2853f1a0 --- /dev/null +++ b/ElectronClient/app/gui/dialogs.js @@ -0,0 +1,33 @@ +const smalltalk = require('smalltalk'); + +class Dialogs { + + async alert(message, title = '') { + await smalltalk.alert(title, message); + } + + async confirm(message, title = '') { + try { + await smalltalk.confirm(title, message); + return true; + } catch (error) { + return false; + } + } + + async prompt(message, title = '', defaultValue = '', options = null) { + if (options === null) options = {}; + + try { + const answer = await smalltalk.prompt(title, message, defaultValue, options); + return answer; + } catch (error) { + return null; + } + } + +} + +const dialogs = new Dialogs(); + +module.exports = dialogs; \ No newline at end of file diff --git a/ElectronClient/app/index.html b/ElectronClient/app/index.html index 6bb72a198..db4f88e75 100644 --- a/ElectronClient/app/index.html +++ b/ElectronClient/app/index.html @@ -6,6 +6,18 @@ + +
diff --git a/ElectronClient/app/locales/de_DE.json b/ElectronClient/app/locales/de_DE.json index fee0b1e6f..47431aaa9 100644 --- a/ElectronClient/app/locales/de_DE.json +++ b/ElectronClient/app/locales/de_DE.json @@ -1 +1 @@ -{"Give focus to next pane":"Das nächste Fenster fokussieren","Give focus to previous pane":"Das vorherige Fenster fokussieren","Enter command line mode":"Zum Terminal-Modus wechseln","Exit command line mode":"Den Terminal-Modus verlassen","Edit the selected note":"Die ausgewählte Notiz bearbeiten","Cancel the current command.":"Den momentanen Befehl abbrechen.","Exit the application.":"Das Programm verlassen.","Delete the currently selected note or notebook.":"Die momentan ausgewählte Notiz(-buch) löschen.","To delete a tag, untag the associated notes.":"Hebe die Markierungen zugehöriger Notizen auf, um eine Markierung zu löschen.","Please select the note or notebook to be deleted first.":"Wähle bitte zuerst eine Notiz oder ein Notizbuch aus, das gelöscht werden soll.","Set a to-do as completed / not completed":"Ein To-Do as abgeschlossen / nicht abgeschlossen markieren","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"[t]oggle [c]onsole between maximized/minimized/hidden/visible.","Search":"Suchen","[t]oggle note [m]etadata.":"Notiz-[M]etadata einschal[t]en.","[M]ake a new [n]ote":"Eine neue [N]otiz [m]achen","[M]ake a new [t]odo":"Ein neues [T]o-Do [m]achen","[M]ake a new note[b]ook":"Ein neues Notiz[b]uch [m]achen","Copy ([Y]ank) the [n]ote to a notebook.":"Die Notiz zu einem Notizbuch kopieren.","Move the note to a notebook.":"Die Notiz zu einem Notizbuch verschieben.","Press Ctrl+D or type \"exit\" to exit the application":"Drücke Strg+D oder schreibe \"exit\", um das Programm zu verlassen","More than one item match \"%s\". Please narrow down your query.":"Mehr als eine Notiz stimmt mit \"%s\" überein. Bitte schränke deine Suche ein.","No notebook selected.":"Kein Notizbuch ausgewählt.","No notebook has been specified.":"Kein Notizbuch wurde angegeben.","Y":"J","n":"n","N":"N","y":"j","Cancelling background synchronisation... Please wait.":"Breche Hintergrund-Synchronisations ab....Bitte warten.","No such command: %s":"No such command: %s","The command \"%s\" is only available in GUI mode":"Der Befehl \"%s\" ist nur im GUI Modus verfügbar","Missing required argument: %s":"Fehlendes benötigtes Argument: %s","%s: %s":"%s: %s","Your choice: ":"Deine Auswahl: ","Invalid answer: %s":"Ungültige Antwort: %s","Attaches the given file to the note.":"Hängt die ausgewählte Datei an die Notiz an.","Cannot find \"%s\".":"Kann \"%s\" nicht finden.","Displays the given note.":"Zeigt die jeweilige Notiz an.","Displays the complete information about note.":"Zeigt alle Informationen über die Notiz an.","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"Zeigt an oder stellt einen Optionswert. Wenn kein [Wert] angegeben ist, wird der Wert vom gegebenenen [Namen] angezeigt. Wenn weder [Name] noch [Wert] gegeben sind, wird eine Liste der momentanen Konfiguration angezeigt.","Also displays unset and hidden config variables.":"Zeige auch nicht angegebene oder versteckte Konfigurationsvariablen an.","%s = %s (%s)":"%s = %s (%s)","%s = %s":"%s = %s","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"Vervielfältigt die Notizen die mit übereinstimmen zu [Notizbuch]. Wenn kein Notizbuch angegeben ist, wird die Notiz in das momentane Notizbuch kopiert.","Marks a to-do as done.":"Markiert ein To-Do als abgeschlossen.","Note is not a to-do: \"%s\"":"Notiz ist kein To-Do: \"%s\"","Edit note.":"Notiz bearbeiten.","No text editor is defined. Please set it using `config editor `":"Kein Textbearbeitungsprogramm angegeben. Bitte lege eines mit `config editor ` fest","No active notebook.":"Kein aktives Notizbuch.","Note does not exist: \"%s\". Create it?":"Notiz \"%s\" existiert nicht. Soll sie erstellt werden?","Starting to edit note. Close the editor to get back to the prompt.":"Beginne die Notiz zu bearbeiten. Schließ das Textbearbeitungsprogramm, um zurück zum Terminal zu kommen.","Note has been saved.":"Die Notiz wurde gespeichert.","Exits the application.":"Schließt das Programm.","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"Exportiert Joplins Datein zu dem angegebenen Pfad. Standardmäßig wird die komplette Datenbank inklusive Notizbüchern, Notizen, Markierungen usw. exportiert.","Exports only the given note.":"Exportiert nur die angegebene Notiz.","Exports only the given notebook.":"Exportiert nur das angegebene Notizbuch.","Displays a geolocation URL for the note.":"Zeigt die Standort-URL der Notiz an.","Displays usage information.":"Zeigt die Benutzungsstatistik an.","Shortcuts are not available in CLI mode.":"","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.","The possible commands are:":"Mögliche Befehle sind:","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"In jedem Befehl können Notizen oder Notizbücher durch ihren Titel oder ihre ID spezifiziert werden, oder durch die Abkürzung `$n` oder `$b` um entweder das momentan augewählte Notizbuch oder die momentan ausgewählte Notiz zu wählen. `$c` kann benutzt werden, um die momentane Auswahl zu verweisen.","To move from one pane to another, press Tab or Shift+Tab.":"Um ein von einem Fenster zu einem anderen zu wechseln, drücke Tab oder Shift+Tab.","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"Benutze die Pfeiltasten und Bild hoch/runter um durch Listen und Texte zu scrollen ( inklusive diesem Terminal ).","To maximise/minimise the console, press \"TC\".":"Um das Terminal zu maximieren/minimieren, drücke \"TC\".","To enter command line mode, press \":\"":"","To exit command line mode, press ESCAPE":"","For the complete list of available keyboard shortcuts, type `help shortcuts`":"Um die komplette Liste von verfügbaren Tastenkürzeln anzuzeigen, tippe `help shortcuts` ein","Imports an Evernote notebook file (.enex file).":"Importiert eine Evernote Notizbuch-Datei (.enex Datei).","Do not ask for confirmation.":"Nicht nach einer Bestätigung fragen.","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"Datei \"%s\" wird importiert in das existierende Notizbuch \"%s\". Fortfahren?","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"Ein neues Notizbuch \"%s\" wird erstellt und die Datei \"%s\" wird in es importiert. Fortfahren?","Found: %d.":"Gefunden: %d.","Created: %d.":"Erstellt: %d.","Updated: %d.":"Aktualisiert: %d.","Skipped: %d.":"Übersprungen: %d.","Resources: %d.":"","Tagged: %d.":"Markiert: %d.","Importing notes...":"Importiere Notizen...","The notes have been imported: %s":"Die Notizen wurden importiert: %s","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"Zeigt die Notizen im momentanen Notizbuch an. Benutze `ls /` um eine Liste aller Notizbücher anzuzeigen.","Displays only the first top notes.":"Zeigt nur die Top- Notizen an.","Sorts the item by (eg. title, updated_time, created_time).":"Sorts the item by (eg. title, updated_time, created_time).","Reverses the sorting order.":"Dreht die Sortierreihenfolge um.","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"Zeige nur bestimmt Typen an. Kann `n` für Notizen sein, `t` für To-Dos, oder `nt` für Notizen und To-Dos ( z.B. würde `-tt` nur To-Dos anzeigen, während `-ttd` Notizen und To-Dos anzeigen würde ).","Either \"text\" or \"json\"":"Entweder \"text\" oder \"json\"","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"","Please select a notebook first.":"Bitte wähle erst ein Notizbuch aus.","Creates a new notebook.":"Erstellt ein neues Notizbuch.","Creates a new note.":"Erstellt eine neue Notiz.","Notes can only be created within a notebook.":"Notizen können nur in einem Notizbuch erstellt werden.","Creates a new to-do.":"Erstellt ein neues To-Do.","Moves the notes matching to [notebook].":"Verschiebt die Notizen, die mit übereinstimmen, zu [Notizbuch]","Renames the given (note or notebook) to .":"Benennt das gegebene ( Notiz oder Notizbuch ) zu um.","Deletes the given notebook.":"Löscht das gegebene Notizbuch.","Deletes the notebook without asking for confirmation.":"Löscht das Notizbuch, ohne nach einer Bestätigung zu fragen.","Delete notebook? All notes within this notebook will also be deleted.":"","Deletes the notes matching .":"Löscht die Notizen, die mit übereinstimmen.","Deletes the notes without asking for confirmation.":"Löscht die Notizen, ohne nach einer Bestätigung zu fragen.","%d notes match this pattern. Delete them?":"%d Notizen stimmen mit diesem Muster überein. Sollen sie gelöscht werden?","Delete note?":"Notiz löschen?","Searches for the given in all the notes.":"Sucht nach dem gegebenen in allen Notizen.","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"Sets the property of the given to the given [value]. Possible properties are:\n\n%s","Displays summary about the notes and notebooks.":"Zeigt eine Zusammenfassung über die Notizen und Notizbücher an.","Synchronises with remote storage.":"Synchronises with remote storage.","Sync to provided target (defaults to sync.target config value)":"Mit dem gegebenen Ziel synchronisieren ( voreingestellt auf den sync.target Optionswert)","Synchronisation is already in progress.":"Synchronisation ist bereits im Gange.","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"Eine Sperrdatei ist vorhanden. Wenn du dir sicher bist, dass keine Synchronisation im Gange ist, kannst du die Sperrdatei \"%s\" löschen und vortfahren.","Authentication was not completed (did not receive an authentication token).":"Authentikation wurde nicht abgeschlossen (keinen Authentikations-Token erhalten).","Synchronisation target: %s (%s)":"Synchronisationsziel: %s (%s)","Cannot initialize synchroniser.":"Kann Synchronisierer nicht initialisieren.","Starting synchronisation...":"Starte Synchronisation...","Cancelling... Please wait.":"Breche ab... Bitte warten."," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":" can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.","Invalid command: \"%s\"":"Ungültiger Befehl: \"%s\""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":" can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.","Marks a to-do as non-completed.":"Makiert ein To-Do als nicht-abgeschlossen.","Switches to [notebook] - all further operations will happen within this notebook.":"Wechselt zu [Notizbuch] - alle weiteren Tätigkeiten werden in diesem Notizbuch verrichtet.","Displays version information":"Zeigt die Versionsnummer an","%s %s (%s)":"%s %s (%s)","Enum":"","Type: %s.":"Typ: %s.","Possible values: %s.":"Mögliche Werte: %s.","Default: %s":"Standard: %s","Possible keys/values:":"Mögliche Werte:","Fatal error:":"Schwerwiegender Fehler:","The application has been authorised - you may now close this browser tab.":"Das Programm wurde authorisiert - Du kannst nun diesen Browsertab schließen.","The application has been successfully authorised.":"Das Programm wurde erfolgreich authorisiert.","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"Bitte öffne die folgende URL in deinem Browser, um das Programm zu authentifizieren. Das Programm wird einen Ordner in \"Apps/Joplin\" erstellen und wird nur in diesem Ordner schreiben und lesen. Es wird weder Zugriff auf Dateien außerhalb dieses Ordners haben, noch auf persönliche Daten. Es werden keine Daten mit Dritten geteilt.","Search:":"Suchen:","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"","File":"Datei","New note":"Neue Notiz","New to-do":"Neues To-Do","New notebook":"Neues Notizbuch","Import Evernote notes":"Evernote Notizen importieren","Evernote Export Files":"","Quit":"Verlassen","Edit":"Bearbeiten","Copy":"Kopieren","Cut":"Ausschneiden","Paste":"Einfügen","Search in all the notes":"Alle Notizen durchsuchen","Tools":"Werkzeuge","Synchronisation status":"Synchronisation status","Options":"Optionen","Help":"Hilfe","Website and documentation":"Webseite und Dokumentation","About Joplin":"Über Joplin","%s %s (%s, %s)":"%s %s (%s, %s)","OK":"OK","Cancel":"Abbrechen","Notes and settings are stored in: %s":"","Save":"","Back":"Zurück","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"Ein neues Notizbuch \"%s\" wird erstellt und die Datei \"%s\" wird in es importiert","Please create a notebook first.":"Bitte erstelle zuerst ein Notizbuch.","Note title:":"Notiz Titel:","Please create a notebook first":"Bitte erstelle zuerst ein Notizbuch","To-do title:":"To-Do Titel:","Notebook title:":"Notizbuch Titel:","Add or remove tags:":"Füge hinzu oder entferne Markierungen:","Separate each tag by a comma.":"Trenne jede Markierung mit einem Komma.","Rename notebook:":"Benne Notizbuch um:","Set alarm:":"Alarm erstellen:","Layout":"Layout","Some items cannot be synchronised.":"Some items cannot be synchronised.","View them now":"","ID":"","Source":"","Created":"Created","Updated":"Updated","Add or remove tags":"Markierungen hinzufügen oder entfernen","Switch between note and to-do type":"Zwischen Notiz und To-Do Typ wechseln","Delete":"Löschen","Delete notes?":"Notizen löschen?","No notes in here. Create one by clicking on \"New note\".":"Hier sind noch keine Notizen. Erstelle eine, indem du auf \"Neue Notiz\" drückst.","There is currently no notebook. Create one by clicking on \"New notebook\".":"There is currently no notebook. Create one by clicking on \"New notebook\".","Unsupported link or message: %s":"Nicht unterstützter Link oder Nachricht: %s","Attach file":"Datei anhängen","Set alarm":"Alarm erstellen","Refresh":"Aktualisieren","Clear":"","OneDrive Login":"OneDrive login","Import":"Importieren","Synchronisation Status":"Synchronisation Status","Remove this tag from all the notes?":"Diese Markierung von allen Notizen löschen?","Remove this search from the sidebar?":"Diese Suche von der Seitenleiste entfernen?","Rename":"Umbenennen","Synchronise":"Synchronisieren","Notebooks":"Notizbücher","Tags":"Markierungen","Searches":"Suchen","Please select where the sync status should be exported to":"Please select where the sync status should be exported to","Usage: %s":"Usage: %s","Unknown flag: %s":"Unbekanntes Argument: %s","File system":"Dateisystem","OneDrive":"OneDrive","OneDrive Dev (For testing only)":"OneDrive Dev ( Nur für Tests )","Unknown log level: %s":"Unbekanntes Loglevel: %s","Unknown level ID: %s":"Unbekannte Level-ID: %s","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"Kann Token nicht erneuern: Authentikationsdaten nicht vorhanden. Ein Neustart der Synchronisation behebt das Problem vielleicht.","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"Konnte nicht mit OneDrive synchronisieren.\n\nDieser Fehler kommt oft vor, wenn OneDrive Business benutzt wird, das leider nicht unterstützt wird.\n\nBitte benutze stattdessen einen normalen OneDrive account.","Cannot access %s":"Kann nicht auf %s zugreifen","Created local items: %d.":"","Updated local items: %d.":"","Created remote items: %d.":"","Updated remote items: %d.":"","Deleted local items: %d.":"","Deleted remote items: %d.":"","State: \"%s\".":"Status: \"%s\".","Cancelling...":"Breche ab...","Completed: %s":"Abgeschlossen: %s","Synchronisation is already in progress. State: %s":"Synchronisation ist bereits im Gange. Status: %s","Conflicts":"Konflikte","A notebook with this title already exists: \"%s\"":"Ein Notizbuch mit diesem Titel existiert bereits : \"%s\"","Notebooks cannot be named \"%s\", which is a reserved title.":"Notizbuch kann nicht \"%s\" genannt werden. Dies ist ein reservierter Titel.","Untitled":"Unbenannt","This note does not have geolocation information.":"Diese Notiz hat keine Standort-Informationen.","Cannot copy note to \"%s\" notebook":"Kann Notiz nicht zu Notizbuch \"%s\" kopieren","Cannot move note to \"%s\" notebook":"Kann Notiz nicht zu Notizbuch \"%s\" verschieben","Text editor":"Textbearbeitungsprogramm","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"Das Textbearbeitungsprogramm, mit dem Notizen geöffnet werden. Wenn keines ausgewählt wurde, wird Joplin versuchen das standard-Textbearbeitungsprogramm zu erkennen.","Language":"Sprache","Date format":"Datumsformat","Time format":"Zeitformat","Theme":"Thema","Light":"Hell","Dark":"Dunkel","Show uncompleted todos on top of the lists":"Unvollständige To-Dos oben in der Liste anzeigen","Save geo-location with notes":"Momentanen Standort zusammen mit Notizen speichern","Synchronisation interval":"Synchronisationsinterval","Disabled":"Deaktiviert","%d minutes":"%d Minuten","%d hour":"%d Stunde","%d hours":"%d Stunden","Automatically update the application":"Die Applikation automatisch aktualisieren","Show advanced options":"Erweiterte Optionen anzeigen","Synchronisation target":"Synchronisationsziel","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"Das Synchronisationsziel, mit dem synchronisiert werden soll. Wenn mit dem Dateisystem synchronisiert werden soll, setz den Wert zu `sync.2.path`, um den Zielpfad zu spezifizieren.","Directory to synchronise with (absolute path)":"","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"Der Pfad, mit dem synchronisiert wird, wenn Dateisystem-Synchronisation aktiviert ist. Siehe `sync.target`.","Invalid option value: \"%s\". Possible values are: %s.":"Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s.","Items that cannot be synchronised":"","\"%s\": \"%s\"":"\"%s\": \"%s\"","Sync status (synced items / total items)":"Synchronisationsstatus (synchronisierte Notizen / vorhandenen Notizen )","%s: %d/%d":"%s: %d/%d","Total: %d/%d":"Insgesamt: %d/%d","Conflicted: %d":"","To delete: %d":"To delete: %d","Folders":"Ordner","%s: %d notes":"%s: %d Notizen","Coming alarms":"Anstehende Alarme","On %s: %s":"Auf %s: %s","There are currently no notes. Create one by clicking on the (+) button.":"Momentan existieren noch keine Notizen. Erstelle eine, indem du auf den (+) Knopf drückst.","Delete these notes?":"Sollen diese Notizen gelöscht werden?","Log":"Log","Status":"Status","Export Debug Report":"Fehlerbreicht exportieren","Configuration":"Konfiguration","Move to notebook...":"Zu Notizbuch verschieben...","Move %d notes to notebook \"%s\"?":"%d Notizen zu dem Notizbuch \"%s\" verschieben?","Select date":"Datum ausswählen","Confirm":"Bestätigen","Cancel synchronisation":"Synchronisation abbrechen","The notebook could not be saved: %s":"Dieses Notizbuch konnte nicht gespeichert werden: %s","Edit notebook":"Notizbuch bearbeiten","This note has been modified:":"Diese Notiz wurde verändert:","Save changes":"Änderungen speichern","Discard changes":"Änderungen verwerfen","Unsupported image type: %s":"Nicht unterstütztes Fotoformat: %s","Attach photo":"Foto anhängen","Attach any file":"Beliebige Datei anhängen","Convert to note":"Zu einer Notiz umwandeln","Convert to todo":"Zu einem To-Do umwandeln","Hide metadata":"Metadaten verstecken","Show metadata":"Metadaten anzeigen","View on map":"Auf der Karte anzeigen","Delete notebook":"Notizbuch löschen","Login with OneDrive":"Mit OneDrive anmelden","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"Drücke auf den (+) Knopf, um eine neue Notiz oder ein neues Notizbuch zu erstellen.","You currently have no notebook. Create one by clicking on (+) button.":"Du hast noch kein Notizbuch. Du kannst eines erstellen, indem du auf den (+) Knopf drückst.","Welcome":"Wilkommen"} \ No newline at end of file +{"Give focus to next pane":"Das nächste Fenster fokussieren","Give focus to previous pane":"Das vorherige Fenster fokussieren","Enter command line mode":"Zum Terminal-Modus wechseln","Exit command line mode":"Den Terminal-Modus verlassen","Edit the selected note":"Die ausgewählte Notiz bearbeiten","Cancel the current command.":"Den momentanen Befehl abbrechen.","Exit the application.":"Das Programm verlassen.","Delete the currently selected note or notebook.":"Die/das momentan ausgewählte Notiz(-buch) löschen.","To delete a tag, untag the associated notes.":"Hebe die Markierungen zugehöriger Notizen auf, um eine Markierung zu löschen.","Please select the note or notebook to be deleted first.":"Wähle bitte zuerst eine Notiz oder ein Notizbuch aus, das gelöscht werden soll.","Set a to-do as completed / not completed":"Ein To-Do as abgeschlossen / nicht abgeschlossen markieren","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"[t]oggle [c]onsole between maximized/minimized/hidden/visible.","Search":"Suchen","[t]oggle note [m]etadata.":"Notiz-[M]etadata einschal[t]en.","[M]ake a new [n]ote":"Eine neue [N]otiz [m]achen","[M]ake a new [t]odo":"Ein neues [T]o-Do [m]achen","[M]ake a new note[b]ook":"Ein neues Notiz[b]uch [m]achen","Copy ([Y]ank) the [n]ote to a notebook.":"Die Notiz zu einem Notizbuch kopieren.","Move the note to a notebook.":"Die Notiz zu einem Notizbuch verschieben.","Press Ctrl+D or type \"exit\" to exit the application":"Drücke Strg+D oder tippe \"exit\", um das Programm zu verlassen","More than one item match \"%s\". Please narrow down your query.":"Mehr als eine Notiz stimmt mit \"%s\" überein. Bitte schränke deine Suche ein.","No notebook selected.":"Kein Notizbuch ausgewählt.","No notebook has been specified.":"Kein Notizbuch wurde angegeben.","Y":"J","n":"n","N":"N","y":"j","Cancelling background synchronisation... Please wait.":"Breche Hintergrund-Synchronisation ab... Bitte warten.","No such command: %s":"Ungültiger Befehl: %s","The command \"%s\" is only available in GUI mode":"Der Befehl \"%s\" ist nur im GUI Modus verfügbar","Missing required argument: %s":"Fehlendes benötigtes Argument: %s","%s: %s":"%s: %s","Your choice: ":"Deine Auswahl: ","Invalid answer: %s":"Ungültige Antwort: %s","Attaches the given file to the note.":"Hängt die ausgewählte Datei an die Notiz an.","Cannot find \"%s\".":"Kann \"%s\" nicht finden.","Displays the given note.":"Zeigt die jeweilige Notiz an.","Displays the complete information about note.":"Zeigt alle Informationen über die Notiz an.","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"Zeigt an oder stellt einen Optionswert. Wenn kein [Wert] angegeben ist, wird der Wert vom gegebenen [Namen] angezeigt. Wenn weder [Name] noch [Wert] gegeben sind, wird eine Liste der momentanen Konfiguration angezeigt.","Also displays unset and hidden config variables.":"Zeigt auch nicht angegebene oder versteckte Konfigurationsvariablen an.","%s = %s (%s)":"%s = %s (%s)","%s = %s":"%s = %s","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"Dupliziert die Notizen die mit übereinstimmen zu [Notizbuch]. Wenn kein Notizbuch angegeben ist, wird die Notiz in das momentane Notizbuch kopiert.","Marks a to-do as done.":"Markiert ein To-Do als abgeschlossen.","Note is not a to-do: \"%s\"":"Notiz ist kein To-Do: \"%s\"","Edit note.":"Notiz bearbeiten.","No text editor is defined. Please set it using `config editor `":"Kein Textverarbeitungsprogramm angegeben. Bitte lege eines mit `config editor ` fest","No active notebook.":"Kein aktives Notizbuch.","Note does not exist: \"%s\". Create it?":"Notiz \"%s\" existiert nicht. Soll sie erstellt werden?","Starting to edit note. Close the editor to get back to the prompt.":"Beginne die Notiz zu bearbeiten. Schließe das Textverarbeitungsprogramm, um zurück zum Terminal zu gelangen.","Note has been saved.":"Die Notiz wurde gespeichert.","Exits the application.":"Schließt das Programm.","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"Exportiert Joplins Dateien zu dem angegebenen Pfad. Standardmäßig wird die komplette Datenbank inklusive Notizbüchern, Notizen, Markierungen und Anhängen exportiert.","Exports only the given note.":"Exportiert nur die angegebene Notiz.","Exports only the given notebook.":"Exportiert nur das angegebene Notizbuch.","Displays a geolocation URL for the note.":"Zeigt die Standort-URL der Notiz an.","Displays usage information.":"Zeigt die Nutzungsstatistik an.","Shortcuts are not available in CLI mode.":"Tastenkürzel sind im CLI Modus nicht verfügbar.","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.","The possible commands are:":"Mögliche Befehle sind:","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"In jedem Befehl können Notizen oder Notizbücher durch ihren Titel oder ihre ID spezifiziert werden, oder durch die Abkürzung `$n` oder `$b` um entweder das momentan ausgewählte Notizbuch oder die momentan ausgewählte Notiz zu wählen. `$c` kann benutzt werden, um auf die momentane Auswahl zu verweisen.","To move from one pane to another, press Tab or Shift+Tab.":"Um ein von einem Fenster zu einem anderen zu wechseln, drücke Tab oder Shift+Tab.","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"Benutze die Pfeiltasten und Bild hoch/runter um durch Listen und Texte zu scrollen (inklusive diesem Terminal).","To maximise/minimise the console, press \"TC\".":"Um das Terminal zu maximieren/minimieren, drücke \"TC\".","To enter command line mode, press \":\"":"Um den Kommandozeilen Modus aufzurufen, drücke \":\"","To exit command line mode, press ESCAPE":"Um den Kommandozeilen Modus zu beenden, drücke ESCAPE","For the complete list of available keyboard shortcuts, type `help shortcuts`":"Um die komplette Liste von verfügbaren Tastenkürzeln anzuzeigen, tippe `help shortcuts` ein","Imports an Evernote notebook file (.enex file).":"Importiert eine Evernote Notizbuch-Datei (.enex Datei).","Do not ask for confirmation.":"Nicht nach einer Bestätigung fragen.","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"Datei \"%s\" wird in das existierende Notizbuch \"%s\" importiert. Fortfahren?","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"Neues Notizbuch \"%s\" wird erstellt und die Datei \"%s\" wird hinein importiert. Fortfahren?","Found: %d.":"Gefunden: %d.","Created: %d.":"Erstellt: %d.","Updated: %d.":"Aktualisiert: %d.","Skipped: %d.":"Übersprungen: %d.","Resources: %d.":"Anhänge: %d.","Tagged: %d.":"Markiert: %d.","Importing notes...":"Importiere Notizen...","The notes have been imported: %s":"Die Notizen wurden importiert: %s","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"Zeigt die Notizen im momentanen Notizbuch an. Benutze `ls /` um eine Liste aller Notizbücher anzuzeigen.","Displays only the first top notes.":"Zeigt nur die ersten Notizen an.","Sorts the item by (eg. title, updated_time, created_time).":"Sortiert nach ( z.B. Titel, Bearbeitungszeitpunkt, Erstellungszeitpunkt)","Reverses the sorting order.":"Dreht die Sortierreihenfolge um.","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"Zeigt nur bestimmte Item Typen an. Kann `n` für Notizen sein, `t` für To-Dos, oder `nt` für Notizen und To-Dos ( z.B. zeigt `-tt` nur To-Dos an, während `-ttd` Notizen und To-Dos anzeigt).","Either \"text\" or \"json\"":"Entweder \"text\" oder \"json\"","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"Verwende ausführliches Listen Format. Das Format lautet: ID, NOTIZEN_ANZAHL (für Notizbuch), DATUM, TODO_BEARBEITET (für To-Dos), TITEL","Please select a notebook first.":"Bitte wähle erst ein Notizbuch aus.","Creates a new notebook.":"Erstellt ein neues Notizbuch.","Creates a new note.":"Erstellt eine neue Notiz.","Notes can only be created within a notebook.":"Notizen können nur in einem Notizbuch erstellt werden.","Creates a new to-do.":"Erstellt ein neues To-Do.","Moves the notes matching to [notebook].":"Verschiebt die Notizen, die mit übereinstimmen, zu [Notizbuch]","Renames the given (note or notebook) to .":"Benennt das angegebene ( Notiz oder Notizbuch ) zu um.","Deletes the given notebook.":"Löscht das ausgewählte Notizbuch.","Deletes the notebook without asking for confirmation.":"Löscht das Notizbuch, ohne nach einer Bestätigung zu fragen.","Delete notebook? All notes within this notebook will also be deleted.":"Notizbuch wirklich löschen? Alle Notizen darin werden ebenfalls gelöscht.","Deletes the notes matching .":"Löscht die Notizen, die mit übereinstimmen.","Deletes the notes without asking for confirmation.":"Löscht die Notizen, ohne nach einer Bestätigung zu fragen.","%d notes match this pattern. Delete them?":"%d Notizen stimmen mit diesem Muster überein. Sollen sie gelöscht werden?","Delete note?":"Notiz löschen?","Searches for the given in all the notes.":"Sucht nach dem angegebenen in allen Notizen.","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"Setzt die Eigenschaft der gegebenen auf den gegebenen [Wert]. Mögliche Werte sind:\n\n%s","Displays summary about the notes and notebooks.":"Zeigt eine Zusammenfassung der Notizen und Notizbücher an.","Synchronises with remote storage.":"Synchronisiert mit Remotespeicher.","Sync to provided target (defaults to sync.target config value)":"Mit dem angegebenen Ziel synchronisieren (voreingestellt auf den sync.target Optionswert)","Synchronisation is already in progress.":"Synchronisation ist bereits im Gange.","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"Eine Sperrdatei ist vorhanden. Wenn du dir sicher bist, dass keine Synchronisation im Gange ist, kannst du die Sperrdatei \"%s\" löschen und fortfahren.","Authentication was not completed (did not receive an authentication token).":"Authentifizierung wurde nicht abgeschlossen (keinen Authentifizierung-Token erhalten).","Synchronisation target: %s (%s)":"Synchronisationsziel: %s (%s)","Cannot initialize synchroniser.":"Kann Synchronisierer nicht initialisieren.","Starting synchronisation...":"Starte Synchronisation...","Cancelling... Please wait.":"Breche ab... Bitte warten."," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":" can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.","Invalid command: \"%s\"":"Ungültiger Befehl: \"%s\""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":" kann entweder \"toggle\" oder \"clear\" sein. Benutze \"toggle\", um ein To-Do abzuschließen, oder es zu beginnen (Wenn das Ziel eine normale Notiz ist, wird diese in ein To-Do umgewandelt). Benutze \"clear\", um es zurück in ein To-Do zu verwandeln.","Marks a to-do as non-completed.":"Makiert ein To-Do als nicht-abgeschlossen.","Switches to [notebook] - all further operations will happen within this notebook.":"Wechselt zu [Notizbuch] - alle weiteren Aktionen werden in diesem Notizbuch ausgeführt.","Displays version information":"Zeigt die Versionsnummer an","%s %s (%s)":"%s %s (%s)","Enum":"","Type: %s.":"Typ: %s.","Possible values: %s.":"Mögliche Werte: %s.","Default: %s":"Standard: %s","Possible keys/values:":"Mögliche Werte:","Fatal error:":"Schwerwiegender Fehler:","The application has been authorised - you may now close this browser tab.":"Das Programm wurde autorisiert - Du kannst diesen Browsertab nun schließen.","The application has been successfully authorised.":"Das Programm wurde erfolgreich autorisiert.","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"Bitte öffne die folgende URL in deinem Browser, um das Programm zu authentifizieren. Das Programm wird einen Ordner in \"Apps/Joplin\" erstellen und wird nur in diesem Ordner schreiben und lesen. Es wird weder Zugriff auf Dateien außerhalb dieses Ordners haben, noch auf andere persönliche Daten. Es werden keine Daten mit Dritten geteilt.","Search:":"Suchen:","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"Willkommen bei Joplin!\n\nTippe `:help shortcuts` für eine Liste der Shortcuts oder `:help` für Nutzungsinformationen ein.\n\nUm zum Beispiel ein Notizbuch zu erstellen, drücke `mb`; um eine Notiz zu erstellen drücke `mn`.","File":"Datei","New note":"Neue Notiz","New to-do":"Neues To-Do","New notebook":"Neues Notizbuch","Import Evernote notes":"Evernote Notizen importieren","Evernote Export Files":"Evernote Export Dateien","Quit":"Verlassen","Edit":"Bearbeiten","Copy":"Kopieren","Cut":"Ausschneiden","Paste":"Einfügen","Search in all the notes":"Alle Notizen durchsuchen","Tools":"Werkzeuge","Synchronisation status":"Status der Synchronisation","Options":"Optionen","Help":"Hilfe","Website and documentation":"Webseite und Dokumentation","About Joplin":"Über Joplin","%s %s (%s, %s)":"%s %s (%s, %s)","OK":"OK","Cancel":"Abbrechen","Notes and settings are stored in: %s":"Notizen und Einstellungen gespeichert in: %s","Save":"Speichern","Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?":"","Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.":"","Disable encryption":"","Enable encryption":"","Master Keys":"","Active":"Aktiv","ID":"ID","Source":"Quelle","Created":"Erstellt","Updated":"Aktualisiert","Password":"Passwort","Password OK":"Passwort OK","Note: Only one master key is going to be used for encryption (the one marked as \"active\"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.":"Hinweis: Nur ein Hauptschlüssel wird für die Verschlüsselung verwendet (der als \"aktiv\" markierte). Jeder der Schlüssel kann für die Entschlüsselung verwendet werden, abhängig davon, wie die jeweiligen Notizen oder Notizbücher ursprünglich verschlüsselt wurden.","Status":"Status","Encryption is:":"","Enabled":"Enabled","Disabled":"Deaktiviert","Back":"Zurück","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"Neues Notizbuch \"%s\" wird erstellt und die Datei \"%s\" wird hinein importiert","Please create a notebook first.":"Bitte erstelle zuerst ein Notizbuch.","Note title:":"Notizen Titel:","Please create a notebook first":"Bitte erstelle zuerst ein Notizbuch","To-do title:":"To-Do Titel:","Notebook title:":"Notizbuch Titel:","Add or remove tags:":"Füge hinzu oder entferne Markierungen:","Separate each tag by a comma.":"Trenne jede Markierung mit einem Komma.","Rename notebook:":"Benne Notizbuch um:","Set alarm:":"Alarm erstellen:","Layout":"Layout","Some items cannot be synchronised.":"Manche Objekte können nicht synchronisiert werden.","View them now":"Zeige sie jetzt an","Some items cannot be decrypted.":"Some items cannot be decrypted.","Set the password":"","Add or remove tags":"Markierungen hinzufügen oder entfernen","Switch between note and to-do type":"Zwischen Notiz und To-Do Typ wechseln","Delete":"Löschen","Delete notes?":"Notizen löschen?","No notes in here. Create one by clicking on \"New note\".":"Hier sind noch keine Notizen. Erstelle eine, indem du auf \"Neue Notiz\" drückst.","There is currently no notebook. Create one by clicking on \"New notebook\".":"Momentan existieren noch keine Notizbücher. Erstelle eines, indem du auf den (+) Knopf drückst.","Unsupported link or message: %s":"Nicht unterstützter Link oder Nachricht: %s","Attach file":"Datei anhängen","Set alarm":"Alarm erstellen","Refresh":"Aktualisieren","Clear":"","OneDrive Login":"OneDrive Login","Import":"Importieren","Synchronisation Status":"Synchronisations Status","Encryption Options":"","Remove this tag from all the notes?":"Diese Markierung von allen Notizen entfernen?","Remove this search from the sidebar?":"Diese Suche von der Seitenleiste entfernen?","Rename":"Umbenennen","Synchronise":"Synchronisieren","Notebooks":"Notizbücher","Tags":"Markierungen","Searches":"Suchen","Please select where the sync status should be exported to":"Bitte wähle aus, wohin der Synchronisations Status exportiert werden soll","Usage: %s":"Nutzung: %s","Unknown flag: %s":"Unbekanntes Argument: %s","File system":"Dateisystem","OneDrive":"OneDrive","OneDrive Dev (For testing only)":"OneDrive Dev (Nur für Tests)","Unknown log level: %s":"Unbekanntes Log Level: %s","Unknown level ID: %s":"Unbekannte Level ID: %s","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"Kann Token nicht erneuern: Authentifikationsdaten nicht vorhanden. Ein Neustart der Synchronisation könnte das Problem beheben.","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"Konnte nicht mit OneDrive synchronisieren.\n\nDieser Fehler kommt oft vor, wenn OneDrive Business benutzt wird, das leider nicht unterstützt wird.\n\nBitte benutze stattdessen einen normalen OneDrive Account.","Cannot access %s":"Kann nicht auf %s zugreifen","Created local items: %d.":"Lokale Objekte erstellt: %d.","Updated local items: %d.":"Lokale Objekte aktualisiert: %d.","Created remote items: %d.":"Remote Objekte erstellt: %d.","Updated remote items: %d.":"Remote Objekte aktualisiert: %d.","Deleted local items: %d.":"Lokale Objekte gelöscht: %d.","Deleted remote items: %d.":"Remote Objekte gelöscht: %d.","State: \"%s\".":"Status: \"%s\".","Cancelling...":"Breche ab...","Completed: %s":"Abgeschlossen: %s","Synchronisation is already in progress. State: %s":"Synchronisation ist bereits im Gange. Status: %s","Conflicts":"Konflikte","A notebook with this title already exists: \"%s\"":"Ein Notizbuch mit diesem Titel existiert bereits : \"%s\"","Notebooks cannot be named \"%s\", which is a reserved title.":"Notizbuch kann nicht \"%s\" genannt werden. Dies ist ein reservierter Titel.","Untitled":"Unbenannt","This note does not have geolocation information.":"Diese Notiz hat keine Standort-Informationen.","Cannot copy note to \"%s\" notebook":"Kann Notiz nicht zu Notizbuch \"%s\" kopieren","Cannot move note to \"%s\" notebook":"Kann Notiz nicht zu Notizbuch \"%s\" verschieben","Text editor":"Textverarbeitungsprogramm","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"Das Textverarbeitungsprogramm, mit dem Notizen geöffnet werden. Wenn keines ausgewählt wurde, wird Joplin versuchen das standard-Textverarbeitungsprogramm zu erkennen.","Language":"Sprache","Date format":"Datumsformat","Time format":"Zeitformat","Theme":"Thema","Light":"Hell","Dark":"Dunkel","Show uncompleted todos on top of the lists":"Zeige unvollständige To-Dos oben in der Liste","Save geo-location with notes":"Momentanen Standort zusammen mit Notizen speichern","Synchronisation interval":"Synchronisationsinterval","%d minutes":"%d Minuten","%d hour":"%d Stunde","%d hours":"%d Stunden","Automatically update the application":"Die Applikation automatisch aktualisieren","Show advanced options":"Erweiterte Optionen anzeigen","Synchronisation target":"Synchronisationsziel","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"Das Synchronisationsziel, mit dem synchronisiert werden soll. Wenn mit dem Dateisystem synchronisiert werden soll, setze den Wert zu `sync.2.path`, um den Zielpfad zu spezifizieren.","Directory to synchronise with (absolute path)":"Verzeichnis zum synchronisieren (absoluter Pfad)","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"Der Pfad, mit dem synchronisiert wird, wenn Dateisystem-Synchronisation aktiviert ist. Siehe `sync.target`.","Invalid option value: \"%s\". Possible values are: %s.":"Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s.","Items that cannot be synchronised":"Objekte können nicht synchronisiert werden","\"%s\": \"%s\"":"\"%s\": \"%s\"","Sync status (synced items / total items)":"Synchronisationsstatus (synchronisierte Objekte / gesamte Objekte)","%s: %d/%d":"%s: %d/%d","Total: %d/%d":"Insgesamt: %d/%d","Conflicted: %d":"In Konflikt %d","To delete: %d":"Zu löschen: %d","Folders":"Ordner","%s: %d notes":"%s: %d Notizen","Coming alarms":"Anstehende Alarme","On %s: %s":"Auf %s: %s","There are currently no notes. Create one by clicking on the (+) button.":"Momentan existieren noch keine Notizen. Erstelle eine, indem du auf den (+) Knopf drückst.","Delete these notes?":"Sollen diese Notizen gelöscht werden?","Log":"Log","Export Debug Report":"Fehlerbreicht exportieren","Configuration":"Konfiguration","Move to notebook...":"In Notizbuch verschieben...","Move %d notes to notebook \"%s\"?":"%d Notizen in das Notizbuch \"%s\" verschieben?","Select date":"Datum auswählen","Confirm":"Bestätigen","Cancel synchronisation":"Synchronisation abbrechen","The notebook could not be saved: %s":"Dieses Notizbuch konnte nicht gespeichert werden: %s","Edit notebook":"Notizbuch bearbeiten","This note has been modified:":"Diese Notiz wurde verändert:","Save changes":"Änderungen speichern","Discard changes":"Änderungen verwerfen","Unsupported image type: %s":"Nicht unterstütztes Fotoformat: %s","Attach photo":"Foto anhängen","Attach any file":"Beliebige Datei anhängen","Convert to note":"In eine Notiz umwandeln","Convert to todo":"In ein To-Do umwandeln","Hide metadata":"Metadaten verstecken","Show metadata":"Metadaten anzeigen","View on map":"Auf der Karte anzeigen","Delete notebook":"Notizbuch löschen","Login with OneDrive":"Mit OneDrive anmelden","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"Drücke auf den (+) Knopf, um eine neue Notiz oder ein neues Notizbuch zu erstellen. Tippe auf die Seitenleiste, um auf deine existierenden Notizbücher zuzugreifen.","You currently have no notebook. Create one by clicking on (+) button.":"Du hast noch kein Notizbuch. Erstelle eines, indem du auf den (+) Knopf drückst.","Welcome":"Willkommen"} \ No newline at end of file diff --git a/ElectronClient/app/locales/en_GB.json b/ElectronClient/app/locales/en_GB.json index 25bafdc80..f060bcfae 100644 --- a/ElectronClient/app/locales/en_GB.json +++ b/ElectronClient/app/locales/en_GB.json @@ -1 +1 @@ -{"Give focus to next pane":"","Give focus to previous pane":"","Enter command line mode":"","Exit command line mode":"","Edit the selected note":"","Cancel the current command.":"","Exit the application.":"","Delete the currently selected note or notebook.":"","To delete a tag, untag the associated notes.":"","Please select the note or notebook to be deleted first.":"","Set a to-do as completed / not completed":"","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"","Search":"","[t]oggle note [m]etadata.":"","[M]ake a new [n]ote":"","[M]ake a new [t]odo":"","[M]ake a new note[b]ook":"","Copy ([Y]ank) the [n]ote to a notebook.":"","Move the note to a notebook.":"","Press Ctrl+D or type \"exit\" to exit the application":"","More than one item match \"%s\". Please narrow down your query.":"","No notebook selected.":"","No notebook has been specified.":"","Y":"","n":"","N":"","y":"","Cancelling background synchronisation... Please wait.":"","No such command: %s":"","The command \"%s\" is only available in GUI mode":"","Missing required argument: %s":"","%s: %s":"","Your choice: ":"","Invalid answer: %s":"","Attaches the given file to the note.":"","Cannot find \"%s\".":"","Displays the given note.":"","Displays the complete information about note.":"","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"","Also displays unset and hidden config variables.":"","%s = %s (%s)":"","%s = %s":"","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"","Marks a to-do as done.":"","Note is not a to-do: \"%s\"":"","Edit note.":"","No text editor is defined. Please set it using `config editor `":"","No active notebook.":"","Note does not exist: \"%s\". Create it?":"","Starting to edit note. Close the editor to get back to the prompt.":"","Note has been saved.":"","Exits the application.":"","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"","Exports only the given note.":"","Exports only the given notebook.":"","Displays a geolocation URL for the note.":"","Displays usage information.":"","Shortcuts are not available in CLI mode.":"","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"","The possible commands are:":"","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"","To move from one pane to another, press Tab or Shift+Tab.":"","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"","To maximise/minimise the console, press \"TC\".":"","To enter command line mode, press \":\"":"","To exit command line mode, press ESCAPE":"","For the complete list of available keyboard shortcuts, type `help shortcuts`":"","Imports an Evernote notebook file (.enex file).":"","Do not ask for confirmation.":"","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"","Found: %d.":"","Created: %d.":"","Updated: %d.":"","Skipped: %d.":"","Resources: %d.":"","Tagged: %d.":"","Importing notes...":"","The notes have been imported: %s":"","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"","Displays only the first top notes.":"","Sorts the item by (eg. title, updated_time, created_time).":"","Reverses the sorting order.":"","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"","Either \"text\" or \"json\"":"","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"","Please select a notebook first.":"","Creates a new notebook.":"","Creates a new note.":"","Notes can only be created within a notebook.":"","Creates a new to-do.":"","Moves the notes matching to [notebook].":"","Renames the given (note or notebook) to .":"","Deletes the given notebook.":"","Deletes the notebook without asking for confirmation.":"","Delete notebook? All notes within this notebook will also be deleted.":"","Deletes the notes matching .":"","Deletes the notes without asking for confirmation.":"","%d notes match this pattern. Delete them?":"","Delete note?":"","Searches for the given in all the notes.":"","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"","Displays summary about the notes and notebooks.":"","Synchronises with remote storage.":"","Sync to provided target (defaults to sync.target config value)":"","Synchronisation is already in progress.":"","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"","Authentication was not completed (did not receive an authentication token).":"","Synchronisation target: %s (%s)":"","Cannot initialize synchroniser.":"","Starting synchronisation...":"","Cancelling... Please wait.":""," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":"","Invalid command: \"%s\"":""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":"","Marks a to-do as non-completed.":"","Switches to [notebook] - all further operations will happen within this notebook.":"","Displays version information":"","%s %s (%s)":"","Enum":"","Type: %s.":"","Possible values: %s.":"","Default: %s":"","Possible keys/values:":"","Fatal error:":"","The application has been authorised - you may now close this browser tab.":"","The application has been successfully authorised.":"","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"","Search:":"","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"","File":"","New note":"","New to-do":"","New notebook":"","Import Evernote notes":"","Evernote Export Files":"","Quit":"","Edit":"","Copy":"","Cut":"","Paste":"","Search in all the notes":"","Tools":"","Synchronisation status":"","Options":"","Help":"","Website and documentation":"","About Joplin":"","%s %s (%s, %s)":"","OK":"","Cancel":"","Notes and settings are stored in: %s":"","Save":"","Back":"","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"","Please create a notebook first.":"","Note title:":"","Please create a notebook first":"","To-do title:":"","Notebook title:":"","Add or remove tags:":"","Separate each tag by a comma.":"","Rename notebook:":"","Set alarm:":"","Layout":"","Some items cannot be synchronised.":"","View them now":"","ID":"","Source":"","Created":"","Updated":"","Add or remove tags":"","Switch between note and to-do type":"","Delete":"","Delete notes?":"","No notes in here. Create one by clicking on \"New note\".":"","There is currently no notebook. Create one by clicking on \"New notebook\".":"","Unsupported link or message: %s":"","Attach file":"","Set alarm":"","Refresh":"","Clear":"","OneDrive Login":"","Import":"","Synchronisation Status":"","Remove this tag from all the notes?":"","Remove this search from the sidebar?":"","Rename":"","Synchronise":"","Notebooks":"","Tags":"","Searches":"","Please select where the sync status should be exported to":"","Usage: %s":"","Unknown flag: %s":"","File system":"","OneDrive":"","OneDrive Dev (For testing only)":"","Unknown log level: %s":"","Unknown level ID: %s":"","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"","Cannot access %s":"","Created local items: %d.":"","Updated local items: %d.":"","Created remote items: %d.":"","Updated remote items: %d.":"","Deleted local items: %d.":"","Deleted remote items: %d.":"","State: \"%s\".":"","Cancelling...":"","Completed: %s":"","Synchronisation is already in progress. State: %s":"","Conflicts":"","A notebook with this title already exists: \"%s\"":"","Notebooks cannot be named \"%s\", which is a reserved title.":"","Untitled":"","This note does not have geolocation information.":"","Cannot copy note to \"%s\" notebook":"","Cannot move note to \"%s\" notebook":"","Text editor":"","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"","Language":"","Date format":"","Time format":"","Theme":"","Light":"","Dark":"","Show uncompleted todos on top of the lists":"","Save geo-location with notes":"","Synchronisation interval":"","Disabled":"","%d minutes":"","%d hour":"","%d hours":"","Automatically update the application":"","Show advanced options":"","Synchronisation target":"","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"","Directory to synchronise with (absolute path)":"","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"","Invalid option value: \"%s\". Possible values are: %s.":"","Items that cannot be synchronised":"","\"%s\": \"%s\"":"","Sync status (synced items / total items)":"","%s: %d/%d":"","Total: %d/%d":"","Conflicted: %d":"","To delete: %d":"","Folders":"","%s: %d notes":"","Coming alarms":"","On %s: %s":"","There are currently no notes. Create one by clicking on the (+) button.":"","Delete these notes?":"","Log":"","Status":"","Export Debug Report":"","Configuration":"","Move to notebook...":"","Move %d notes to notebook \"%s\"?":"","Select date":"","Confirm":"","Cancel synchronisation":"","The notebook could not be saved: %s":"","Edit notebook":"","This note has been modified:":"","Save changes":"","Discard changes":"","Unsupported image type: %s":"","Attach photo":"","Attach any file":"","Convert to note":"","Convert to todo":"","Hide metadata":"","Show metadata":"","View on map":"","Delete notebook":"","Login with OneDrive":"","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"","You currently have no notebook. Create one by clicking on (+) button.":"","Welcome":""} \ No newline at end of file +{"Give focus to next pane":"","Give focus to previous pane":"","Enter command line mode":"","Exit command line mode":"","Edit the selected note":"","Cancel the current command.":"","Exit the application.":"","Delete the currently selected note or notebook.":"","To delete a tag, untag the associated notes.":"","Please select the note or notebook to be deleted first.":"","Set a to-do as completed / not completed":"","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"","Search":"","[t]oggle note [m]etadata.":"","[M]ake a new [n]ote":"","[M]ake a new [t]odo":"","[M]ake a new note[b]ook":"","Copy ([Y]ank) the [n]ote to a notebook.":"","Move the note to a notebook.":"","Press Ctrl+D or type \"exit\" to exit the application":"","More than one item match \"%s\". Please narrow down your query.":"","No notebook selected.":"","No notebook has been specified.":"","Y":"","n":"","N":"","y":"","Cancelling background synchronisation... Please wait.":"","No such command: %s":"","The command \"%s\" is only available in GUI mode":"","Missing required argument: %s":"","%s: %s":"","Your choice: ":"","Invalid answer: %s":"","Attaches the given file to the note.":"","Cannot find \"%s\".":"","Displays the given note.":"","Displays the complete information about note.":"","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"","Also displays unset and hidden config variables.":"","%s = %s (%s)":"","%s = %s":"","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"","Marks a to-do as done.":"","Note is not a to-do: \"%s\"":"","Edit note.":"","No text editor is defined. Please set it using `config editor `":"","No active notebook.":"","Note does not exist: \"%s\". Create it?":"","Starting to edit note. Close the editor to get back to the prompt.":"","Note has been saved.":"","Exits the application.":"","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"","Exports only the given note.":"","Exports only the given notebook.":"","Displays a geolocation URL for the note.":"","Displays usage information.":"","Shortcuts are not available in CLI mode.":"","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"","The possible commands are:":"","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"","To move from one pane to another, press Tab or Shift+Tab.":"","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"","To maximise/minimise the console, press \"TC\".":"","To enter command line mode, press \":\"":"","To exit command line mode, press ESCAPE":"","For the complete list of available keyboard shortcuts, type `help shortcuts`":"","Imports an Evernote notebook file (.enex file).":"","Do not ask for confirmation.":"","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"","Found: %d.":"","Created: %d.":"","Updated: %d.":"","Skipped: %d.":"","Resources: %d.":"","Tagged: %d.":"","Importing notes...":"","The notes have been imported: %s":"","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"","Displays only the first top notes.":"","Sorts the item by (eg. title, updated_time, created_time).":"","Reverses the sorting order.":"","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"","Either \"text\" or \"json\"":"","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"","Please select a notebook first.":"","Creates a new notebook.":"","Creates a new note.":"","Notes can only be created within a notebook.":"","Creates a new to-do.":"","Moves the notes matching to [notebook].":"","Renames the given (note or notebook) to .":"","Deletes the given notebook.":"","Deletes the notebook without asking for confirmation.":"","Delete notebook? All notes within this notebook will also be deleted.":"","Deletes the notes matching .":"","Deletes the notes without asking for confirmation.":"","%d notes match this pattern. Delete them?":"","Delete note?":"","Searches for the given in all the notes.":"","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"","Displays summary about the notes and notebooks.":"","Synchronises with remote storage.":"","Sync to provided target (defaults to sync.target config value)":"","Synchronisation is already in progress.":"","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"","Authentication was not completed (did not receive an authentication token).":"","Synchronisation target: %s (%s)":"","Cannot initialize synchroniser.":"","Starting synchronisation...":"","Cancelling... Please wait.":""," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":"","Invalid command: \"%s\"":""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":"","Marks a to-do as non-completed.":"","Switches to [notebook] - all further operations will happen within this notebook.":"","Displays version information":"","%s %s (%s)":"","Enum":"","Type: %s.":"","Possible values: %s.":"","Default: %s":"","Possible keys/values:":"","Fatal error:":"","The application has been authorised - you may now close this browser tab.":"","The application has been successfully authorised.":"","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"","Search:":"","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"","File":"","New note":"","New to-do":"","New notebook":"","Import Evernote notes":"","Evernote Export Files":"","Quit":"","Edit":"","Copy":"","Cut":"","Paste":"","Search in all the notes":"","Tools":"","Synchronisation status":"","Options":"","Help":"","Website and documentation":"","About Joplin":"","%s %s (%s, %s)":"","OK":"","Cancel":"","Notes and settings are stored in: %s":"","Save":"","Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?":"","Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.":"","Disable encryption":"","Enable encryption":"","Master Keys":"","Active":"","ID":"","Source":"","Created":"","Updated":"","Password":"","Password OK":"","Note: Only one master key is going to be used for encryption (the one marked as \"active\"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.":"","Status":"","Encryption is:":"","Enabled":"","Disabled":"","Back":"","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"","Please create a notebook first.":"","Note title:":"","Please create a notebook first":"","To-do title:":"","Notebook title:":"","Add or remove tags:":"","Separate each tag by a comma.":"","Rename notebook:":"","Set alarm:":"","Layout":"","Some items cannot be synchronised.":"","View them now":"","Some items cannot be decrypted.":"","Set the password":"","Add or remove tags":"","Switch between note and to-do type":"","Delete":"","Delete notes?":"","No notes in here. Create one by clicking on \"New note\".":"","There is currently no notebook. Create one by clicking on \"New notebook\".":"","Unsupported link or message: %s":"","Attach file":"","Set alarm":"","Refresh":"","Clear":"","OneDrive Login":"","Import":"","Synchronisation Status":"","Encryption Options":"","Remove this tag from all the notes?":"","Remove this search from the sidebar?":"","Rename":"","Synchronise":"","Notebooks":"","Tags":"","Searches":"","Please select where the sync status should be exported to":"","Usage: %s":"","Unknown flag: %s":"","File system":"","OneDrive":"","OneDrive Dev (For testing only)":"","Unknown log level: %s":"","Unknown level ID: %s":"","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"","Cannot access %s":"","Created local items: %d.":"","Updated local items: %d.":"","Created remote items: %d.":"","Updated remote items: %d.":"","Deleted local items: %d.":"","Deleted remote items: %d.":"","State: \"%s\".":"","Cancelling...":"","Completed: %s":"","Synchronisation is already in progress. State: %s":"","Conflicts":"","A notebook with this title already exists: \"%s\"":"","Notebooks cannot be named \"%s\", which is a reserved title.":"","Untitled":"","This note does not have geolocation information.":"","Cannot copy note to \"%s\" notebook":"","Cannot move note to \"%s\" notebook":"","Text editor":"","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"","Language":"","Date format":"","Time format":"","Theme":"","Light":"","Dark":"","Show uncompleted todos on top of the lists":"","Save geo-location with notes":"","Synchronisation interval":"","%d minutes":"","%d hour":"","%d hours":"","Automatically update the application":"","Show advanced options":"","Synchronisation target":"","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"","Directory to synchronise with (absolute path)":"","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"","Invalid option value: \"%s\". Possible values are: %s.":"","Items that cannot be synchronised":"","\"%s\": \"%s\"":"","Sync status (synced items / total items)":"","%s: %d/%d":"","Total: %d/%d":"","Conflicted: %d":"","To delete: %d":"","Folders":"","%s: %d notes":"","Coming alarms":"","On %s: %s":"","There are currently no notes. Create one by clicking on the (+) button.":"","Delete these notes?":"","Log":"","Export Debug Report":"","Configuration":"","Move to notebook...":"","Move %d notes to notebook \"%s\"?":"","Select date":"","Confirm":"","Cancel synchronisation":"","The notebook could not be saved: %s":"","Edit notebook":"","This note has been modified:":"","Save changes":"","Discard changes":"","Unsupported image type: %s":"","Attach photo":"","Attach any file":"","Convert to note":"","Convert to todo":"","Hide metadata":"","Show metadata":"","View on map":"","Delete notebook":"","Login with OneDrive":"","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"","You currently have no notebook. Create one by clicking on (+) button.":"","Welcome":""} \ No newline at end of file diff --git a/ElectronClient/app/locales/es_CR.json b/ElectronClient/app/locales/es_CR.json index 79af2a1bd..c74a4107f 100644 --- a/ElectronClient/app/locales/es_CR.json +++ b/ElectronClient/app/locales/es_CR.json @@ -1 +1 @@ -{"Give focus to next pane":"Dar enfoque al siguiente panel","Give focus to previous pane":"Dar enfoque al panel anterior","Enter command line mode":"Entrar modo linea de comandos","Exit command line mode":"Salir modo linea de comandos","Edit the selected note":"Editar la nota seleccionada","Cancel the current command.":"Cancelar el comando actual.","Exit the application.":"Salir de la aplicación.","Delete the currently selected note or notebook.":"Eliminar la nota o libreta seleccionada.","To delete a tag, untag the associated notes.":"Para eliminar una etiqueta, desmarca las notas asociadas.","Please select the note or notebook to be deleted first.":"Por favor selecciona la nota o libreta a elliminar.","Set a to-do as completed / not completed":"Set a to-do as completed / not completed","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"[c]ambia la [c]onsola entre maximizado/minimizado/oculto/visible.","Search":"Buscar","[t]oggle note [m]etadata.":"[c]ambia los [m]etadatos de una nota.","[M]ake a new [n]ote":"[H]acer una [n]ota nueva","[M]ake a new [t]odo":"[H]acer una nueva [t]area","[M]ake a new note[b]ook":"[H]acer una nueva [l]ibreta","Copy ([Y]ank) the [n]ote to a notebook.":"Copiar ([Y]ank) la [n]ota a una libreta.","Move the note to a notebook.":"Mover la nota a una libreta.","Press Ctrl+D or type \"exit\" to exit the application":"Presiona Ctrl+D o escribe \"salir\" para salir de la aplicación","More than one item match \"%s\". Please narrow down your query.":"Más de un artículo coincide con \"%s\". Por favor acortar tu consulta.","No notebook selected.":"Ninguna libreta seleccionada","No notebook has been specified.":"Ninguna libre fue especificada","Y":"Y","n":"n","N":"N","y":"y","Cancelling background synchronisation... Please wait.":"Cancelando sincronización de segundo plano... Por favor espere.","No such command: %s":"No such command: %s","The command \"%s\" is only available in GUI mode":"The command \"%s\" is only available in GUI mode","Missing required argument: %s":"Falta un argumento requerido: %s","%s: %s":"%s: %s","Your choice: ":"Tu elección: ","Invalid answer: %s":"Respuesta inválida: %s","Attaches the given file to the note.":"Adjuntar archivo a la nota.","Cannot find \"%s\".":"No se encuentra \"%s\".","Displays the given note.":"Mostrar la nota dada.","Displays the complete information about note.":"Mostrar la información completa acerca de la nota.","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"Obtener o configurar un valor. Si no se provee el [valor], se mostrará el valor de [nombre]. Si no se provee [nombre] ni [valor], se listara la configuración actual.","Also displays unset and hidden config variables.":"También muestra variables ocultas o no configuradas.","%s = %s (%s)":"%s = %s (%s)","%s = %s":"%s = %s","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"Duplica las notas que coincidan con en la libreta. Si no se especifica una libreta la nota se duplica en la libreta actual.","Marks a to-do as done.":"Marca una tarea como hecha.","Note is not a to-do: \"%s\"":"Una nota no es una tarea: \"%s\"","Edit note.":"Editar una nota.","No text editor is defined. Please set it using `config editor `":"No hay editor de texto definido. Por favor configure uno usando `config editor `","No active notebook.":"No hay libreta activa.","Note does not exist: \"%s\". Create it?":"La nota no existe: \"%s\". Crearla?","Starting to edit note. Close the editor to get back to the prompt.":"Iniciando a editar una nota. Cierra el editor para regresar al prompt.","Note has been saved.":"La nota a sido guardada.","Exits the application.":"Sale de la aplicación.","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"Exportar datos de Joplin al directorio indicado. Por defecto, se exportará la base de datos completa incluyendo libretas, notas, etiquetas y recursos.","Exports only the given note.":"Exportar unicamente la nota indicada.","Exports only the given notebook.":"Exportar unicamente la libreta indicada.","Displays a geolocation URL for the note.":"Mostrar geolocalización de la URL para la nota.","Displays usage information.":"Muestra información de uso.","Shortcuts are not available in CLI mode.":"Atajos no disponibles en modo CLI.","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.","The possible commands are:":"Los posibles comandos son:","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"Con cualquier comando, una nota o libreta puede ser referida por titulo o ID, o utilizando atajos `$n` o `$b`, respectivamente, para la nota o libreta seleccionada se puede usar `$c` para hacer referencia al artículo seleccionado.","To move from one pane to another, press Tab or Shift+Tab.":"Para mover desde un panel a otro, presiona Tab o Shift+Tab.","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"Para desplazar en las listas y areas de texto ( incluyendo la consola ) utilice las flechas y re pág/av pág.","To maximise/minimise the console, press \"TC\".":"Para maximizar/minimizar la consola, presiona \"TC\".","To enter command line mode, press \":\"":"Para entrar a modo linea de comando, presiona \":\"","To exit command line mode, press ESCAPE":"Para salir de modo linea de comando, presiona ESCAPE","For the complete list of available keyboard shortcuts, type `help shortcuts`":"Para una lista completa de los atajos de teclado disponibles, escribe `help shortcuts`","Imports an Evernote notebook file (.enex file).":"Importar una libreta de Evernote (archivo .enex).","Do not ask for confirmation.":"No preguntar por confirmación.","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"El archivo \"%s\" será importado dentro de la libreta existente \"%s\". Continuar?","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"Nueva libreta \"%s\" será creada y el archivo \"%s\" será importado dentro de ella. Continuar?","Found: %d.":"Encontrado: %d.","Created: %d.":"Creado: %d.","Updated: %d.":"Actualizado: %d.","Skipped: %d.":"Omitido: %d.","Resources: %d.":"Recursos: %d.","Tagged: %d.":"Etiquetado: %d.","Importing notes...":"Importando notas...","The notes have been imported: %s":"Las notas han sido importadas: %s","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"Muestra las notas en la libreta actual. Usa `ls /` para mostrar la lista de libretas.","Displays only the first top notes.":"Muestra las primeras notas.","Sorts the item by (eg. title, updated_time, created_time).":"Ordena los artículos por campo ( ej. título, fecha de actualización, fecha de creación).","Reverses the sorting order.":"Invierte el orden.","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"Muestra unicamente los artículos de los tipos especificados. Pueden ser `n` para notas, `t` para tareas, o `nt` para libretas y tareas (ej. `-tt` mostrará unicamente las tareas, mientras `-ttd` mostrará notas y tareas).","Either \"text\" or \"json\"":"Puede ser \"text\" o \"json\"","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"Usar formato largo de lista. El formato es ID, NOTE_COUNT ( para libretas), DATE,TODO_CHECKED ( para tareas), TITLE","Please select a notebook first.":"Por favor selecciona la libreta.","Creates a new notebook.":"Crea una nueva libreta.","Creates a new note.":"Crea una nueva nota.","Notes can only be created within a notebook.":"Notas solamente pueden ser creadas dentro de una libreta.","Creates a new to-do.":"Crea una nueva lista de tareas.","Moves the notes matching to [notebook].":"Mueve las notas que coincidan con para la [libreta].","Renames the given (note or notebook) to .":"Renombre el artículo dado (nota o libreta) a .","Deletes the given notebook.":"Elimina la libreta dada.","Deletes the notebook without asking for confirmation.":"Elimina una libreta sin pedir confirmación.","Delete notebook? All notes within this notebook will also be deleted.":"","Deletes the notes matching .":"Elimina las notas que coinciden con .","Deletes the notes without asking for confirmation.":"Elimina las notas sin pedir confirmación.","%d notes match this pattern. Delete them?":"%d notas coinciden con el patron. Eliminarlas?","Delete note?":"Eliminar nota?","Searches for the given in all the notes.":"Buscar el patron en todas las notas.","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"Sets the property of the given to the given [value]. Possible properties are:\n\n%s","Displays summary about the notes and notebooks.":"Muestra un resumen acerca de las notas y las libretas.","Synchronises with remote storage.":"Sincronizar con almacenamiento remoto.","Sync to provided target (defaults to sync.target config value)":"Sincronizar con objetivo proveído ( por defecto al valor de configuración sync.target)","Synchronisation is already in progress.":"Sincronzación en progreso.","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.","Authentication was not completed (did not receive an authentication token).":"Autenticación no completada (no se recibió token de autenticación).","Synchronisation target: %s (%s)":"Objetivo de sincronización: %s (%s)","Cannot initialize synchroniser.":"No se puede inicializar sincronizador.","Starting synchronisation...":"Iniciando sincronización...","Cancelling... Please wait.":"Cancelando... Por favor espere."," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":" puede ser \"add\", \"remove\" o \"list\" para asignar o eliminar [tag] de [note], o para listar las notas asociadas con [tag]. El comando `tag list` puede ser usado para listar todas las etiquetas.","Invalid command: \"%s\"":"Comando inválido: \"%s\""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":" puede ser \"toggle\" o \"clear\". Usa \"toggle\" para cambiar la tarea dada entre estado completado y sin completar. ( Si el objetivo es una nota regular se convertirá en una tarea). Usa \"clear\" para convertir la tarea a una nota regular. ","Marks a to-do as non-completed.":"Marcar una tarea como no completada.","Switches to [notebook] - all further operations will happen within this notebook.":"Cambia una [libreta] - todas las demás operaciones se realizan en ésta libreta.","Displays version information":"Muestra información de la versión","%s %s (%s)":"%s %s (%s)","Enum":"Enumerar","Type: %s.":"Tipo: %s.","Possible values: %s.":"Posibles valores: %s.","Default: %s":"Por defecto: %s","Possible keys/values:":"Teclas/valores posbiles:","Fatal error:":"Error fatal:","The application has been authorised - you may now close this browser tab.":"La aplicación ha sido autorizada - ahora puedes cerrar esta pestaña de tu navegador.","The application has been successfully authorised.":"La aplicacion ha sido autorizada exitosamente.","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.","Search:":"Bucar:","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"","File":"Archivo","New note":"Nueva nota","New to-do":"Nueva lista de tareas","New notebook":"Nueva libreta","Import Evernote notes":"Importar notas de Evernote","Evernote Export Files":"Exportar archivos de Evernote","Quit":"Salir","Edit":"Editar","Copy":"Copiar","Cut":"Cortar","Paste":"Pegar","Search in all the notes":"Buscar en todas las notas","Tools":"Herramientas","Synchronisation status":"Synchronisation status","Options":"Opciones","Help":"Ayuda","Website and documentation":"Sitio web y documentacion","About Joplin":"Acerca de Joplin","%s %s (%s, %s)":"%s %s (%s, %s)","OK":"Ok","Cancel":"Cancelar","Notes and settings are stored in: %s":"","Save":"","Back":"Back","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"New notebook \"%s\" will be created and file \"%s\" will be imported into it","Please create a notebook first.":"Por favor crea una libreta primero.","Note title:":"Título de nota:","Please create a notebook first":"Por favor crea una libreta primero","To-do title:":"Títuto de lista de tareas:","Notebook title:":"Título de libreta:","Add or remove tags:":"Agregar o borrar etiquetas: ","Separate each tag by a comma.":"Separar cada etiqueta por una coma.","Rename notebook:":"Renombrar libreta:","Set alarm:":"Ajustar alarma:","Layout":"Diseño","Some items cannot be synchronised.":"Some items cannot be synchronised.","View them now":"","ID":"","Source":"","Created":"Created","Updated":"Updated","Add or remove tags":"Agregar o borrar etiquetas","Switch between note and to-do type":"Switch between note and to-do type","Delete":"Eliminar","Delete notes?":"Eliminar notas?","No notes in here. Create one by clicking on \"New note\".":"No hay notas aqui. Crea una dando click en \"Nueva nota\".","There is currently no notebook. Create one by clicking on \"New notebook\".":"There is currently no notebook. Create one by clicking on \"New notebook\".","Unsupported link or message: %s":"Enlace o mensaje sin soporte: %s","Attach file":"Adjuntar archivo","Set alarm":"Ajustar alarma","Refresh":"Refrescar","Clear":"Limpiar","OneDrive Login":"Inicio de sesión de OneDrive","Import":"Importar","Synchronisation Status":"Synchronisation Status","Remove this tag from all the notes?":"Remover esta etiqueta de todas las notas?","Remove this search from the sidebar?":"Remover esta busqueda de la barra lateral?","Rename":"Renombrar","Synchronise":"Sincronizar","Notebooks":"Libretas","Tags":"Etiquetas","Searches":"Busquedas","Please select where the sync status should be exported to":"Please select where the sync status should be exported to","Usage: %s":"Uso: %s","Unknown flag: %s":"Etiqueta desconocida: %s","File system":"Sistema de archivos","OneDrive":"OneDrive","OneDrive Dev (For testing only)":"OneDrive Dev (For testing only)","Unknown log level: %s":"Nivel de log desconocido: %s","Unknown level ID: %s":"Nivel de ID desconocido: %s","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.","Cannot access %s":"Cannot access %s","Created local items: %d.":"Artículos locales creados: %d.","Updated local items: %d.":"Artículos locales actualizados: %d.","Created remote items: %d.":"Artículos remotos creados: %d.","Updated remote items: %d.":"Artículos remotos actualizados: %d.","Deleted local items: %d.":"Artículos locales borrados: %d.","Deleted remote items: %d.":"Artículos remotos borrados: %d.","State: \"%s\".":"Estado: \"%s\".","Cancelling...":"Cancelando....","Completed: %s":"Completado: %s","Synchronisation is already in progress. State: %s":"La sincronizacion ya esta en progreso. Estod: %s","Conflicts":"Conflictos","A notebook with this title already exists: \"%s\"":"Ya existe una libreta con este nombre: \"%s\"","Notebooks cannot be named \"%s\", which is a reserved title.":"Notebooks cannot be named \"%s\", which is a reserved title.","Untitled":"Intitulado","This note does not have geolocation information.":"Esta nota no tiene informacion de geolocalización.","Cannot copy note to \"%s\" notebook":"Cannot copy note to \"%s\" notebook","Cannot move note to \"%s\" notebook":"Cannot move note to \"%s\" notebook","Text editor":"Editor de texto","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.","Language":"Lenguaje","Date format":"Formato de fecha","Time format":"Formato de hora","Theme":"Tema","Light":"Claro","Dark":"Oscuro","Show uncompleted todos on top of the lists":"Show uncompleted todos on top of the lists","Save geo-location with notes":"Guardar notas con geo-licalización","Synchronisation interval":"Intervalo de sincronización","Disabled":"Deshabilitado","%d minutes":"%d minutos","%d hour":"%d hora","%d hours":"%d horas","Automatically update the application":"Actualizacion automatica de la aplicación","Show advanced options":"Mostrar opciones ","Synchronisation target":"Sincronización de objetivo","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.","Directory to synchronise with (absolute path)":"","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"La ubicacion para sincronizar cuando el sistema de archivo tenga habilitada la sincronización. Ver `sync.target`.","Invalid option value: \"%s\". Possible values are: %s.":"Valor inválido de opción: \"%s\". Los válores inválidos son: %s.","Items that cannot be synchronised":"","\"%s\": \"%s\"":"","Sync status (synced items / total items)":"Estatus de sincronización (artículos sincronizados / total de artículos)","%s: %d/%d":"%s: %d/%d","Total: %d/%d":"Total: %d/%d","Conflicted: %d":"Conflictivo: %d","To delete: %d":"Borrar: %d","Folders":"Directorios","%s: %d notes":"%s: %d notas","Coming alarms":"Coming alarms","On %s: %s":"En %s: %s","There are currently no notes. Create one by clicking on the (+) button.":"There are currently no notes. Create one by clicking on the (+) button.","Delete these notes?":"Borrar estas notas?","Log":"Log","Status":"Estatus","Export Debug Report":"Export Debug Report","Configuration":"Configuracion","Move to notebook...":"Mover a libreta....","Move %d notes to notebook \"%s\"?":"Mover %d notas a libreta \"%s\"?","Select date":"Seleccionar fecha","Confirm":"Confirmar","Cancel synchronisation":"Sincronizacion cancelada","The notebook could not be saved: %s":"Esta libreta no pudo ser guardada: %s","Edit notebook":"Editar libreta","This note has been modified:":"Esta nota ha sido modificada:","Save changes":"Guardar cambios","Discard changes":"Descartar cambios","Unsupported image type: %s":"Tipo de imagen no soportado: %s","Attach photo":"Adjuntar foto","Attach any file":"Adjuntar cualquier archivo","Convert to note":"Convertir a nota","Convert to todo":"Convertir a lista de tareas","Hide metadata":"Ocultar metadata","Show metadata":"Mostrar metadata","View on map":"Ver un mapa","Delete notebook":"Borrar libreta","Login with OneDrive":"Loguear con OneDrive","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.","You currently have no notebook. Create one by clicking on (+) button.":"You currently have no notebook. Create one by clicking on (+) button.","Welcome":"Bienvenido"} \ No newline at end of file +{"Give focus to next pane":"Dar enfoque al siguiente panel","Give focus to previous pane":"Dar enfoque al panel anterior","Enter command line mode":"Entrar modo linea de comandos","Exit command line mode":"Salir modo linea de comandos","Edit the selected note":"Editar la nota seleccionada","Cancel the current command.":"Cancelar el comando actual.","Exit the application.":"Salir de la aplicación.","Delete the currently selected note or notebook.":"Eliminar la nota o libreta seleccionada.","To delete a tag, untag the associated notes.":"Para eliminar una etiqueta, desmarca las notas asociadas.","Please select the note or notebook to be deleted first.":"Por favor selecciona la nota o libreta a elliminar.","Set a to-do as completed / not completed":"Set a to-do as completed / not completed","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"[c]ambia la [c]onsola entre maximizado/minimizado/oculto/visible.","Search":"Buscar","[t]oggle note [m]etadata.":"[c]ambia los [m]etadatos de una nota.","[M]ake a new [n]ote":"[H]acer una [n]ota nueva","[M]ake a new [t]odo":"[H]acer una nueva [t]area","[M]ake a new note[b]ook":"[H]acer una nueva [l]ibreta","Copy ([Y]ank) the [n]ote to a notebook.":"Copiar ([Y]ank) la [n]ota a una libreta.","Move the note to a notebook.":"Mover la nota a una libreta.","Press Ctrl+D or type \"exit\" to exit the application":"Presiona Ctrl+D o escribe \"salir\" para salir de la aplicación","More than one item match \"%s\". Please narrow down your query.":"Más de un artículo coincide con \"%s\". Por favor acortar tu consulta.","No notebook selected.":"Ninguna libreta seleccionada","No notebook has been specified.":"Ninguna libre fue especificada","Y":"Y","n":"n","N":"N","y":"y","Cancelling background synchronisation... Please wait.":"Cancelando sincronización de segundo plano... Por favor espere.","No such command: %s":"No such command: %s","The command \"%s\" is only available in GUI mode":"The command \"%s\" is only available in GUI mode","Missing required argument: %s":"Falta un argumento requerido: %s","%s: %s":"%s: %s","Your choice: ":"Tu elección: ","Invalid answer: %s":"Respuesta inválida: %s","Attaches the given file to the note.":"Adjuntar archivo a la nota.","Cannot find \"%s\".":"No se encuentra \"%s\".","Displays the given note.":"Mostrar la nota dada.","Displays the complete information about note.":"Mostrar la información completa acerca de la nota.","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"Obtener o configurar un valor. Si no se provee el [valor], se mostrará el valor de [nombre]. Si no se provee [nombre] ni [valor], se listara la configuración actual.","Also displays unset and hidden config variables.":"También muestra variables ocultas o no configuradas.","%s = %s (%s)":"%s = %s (%s)","%s = %s":"%s = %s","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"Duplica las notas que coincidan con en la libreta. Si no se especifica una libreta la nota se duplica en la libreta actual.","Marks a to-do as done.":"Marca una tarea como hecha.","Note is not a to-do: \"%s\"":"Una nota no es una tarea: \"%s\"","Edit note.":"Editar una nota.","No text editor is defined. Please set it using `config editor `":"No hay editor de texto definido. Por favor configure uno usando `config editor `","No active notebook.":"No hay libreta activa.","Note does not exist: \"%s\". Create it?":"La nota no existe: \"%s\". Crearla?","Starting to edit note. Close the editor to get back to the prompt.":"Iniciando a editar una nota. Cierra el editor para regresar al prompt.","Note has been saved.":"La nota a sido guardada.","Exits the application.":"Sale de la aplicación.","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"Exportar datos de Joplin al directorio indicado. Por defecto, se exportará la base de datos completa incluyendo libretas, notas, etiquetas y recursos.","Exports only the given note.":"Exportar unicamente la nota indicada.","Exports only the given notebook.":"Exportar unicamente la libreta indicada.","Displays a geolocation URL for the note.":"Mostrar geolocalización de la URL para la nota.","Displays usage information.":"Muestra información de uso.","Shortcuts are not available in CLI mode.":"Atajos no disponibles en modo CLI.","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.","The possible commands are:":"Los posibles comandos son:","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"Con cualquier comando, una nota o libreta puede ser referida por titulo o ID, o utilizando atajos `$n` o `$b`, respectivamente, para la nota o libreta seleccionada se puede usar `$c` para hacer referencia al artículo seleccionado.","To move from one pane to another, press Tab or Shift+Tab.":"Para mover desde un panel a otro, presiona Tab o Shift+Tab.","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"Para desplazar en las listas y areas de texto ( incluyendo la consola ) utilice las flechas y re pág/av pág.","To maximise/minimise the console, press \"TC\".":"Para maximizar/minimizar la consola, presiona \"TC\".","To enter command line mode, press \":\"":"Para entrar a modo linea de comando, presiona \":\"","To exit command line mode, press ESCAPE":"Para salir de modo linea de comando, presiona ESCAPE","For the complete list of available keyboard shortcuts, type `help shortcuts`":"Para una lista completa de los atajos de teclado disponibles, escribe `help shortcuts`","Imports an Evernote notebook file (.enex file).":"Importar una libreta de Evernote (archivo .enex).","Do not ask for confirmation.":"No preguntar por confirmación.","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"El archivo \"%s\" será importado dentro de la libreta existente \"%s\". Continuar?","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"Nueva libreta \"%s\" será creada y el archivo \"%s\" será importado dentro de ella. Continuar?","Found: %d.":"Encontrado: %d.","Created: %d.":"Creado: %d.","Updated: %d.":"Actualizado: %d.","Skipped: %d.":"Omitido: %d.","Resources: %d.":"Recursos: %d.","Tagged: %d.":"Etiquetado: %d.","Importing notes...":"Importando notas...","The notes have been imported: %s":"Las notas han sido importadas: %s","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"Muestra las notas en la libreta actual. Usa `ls /` para mostrar la lista de libretas.","Displays only the first top notes.":"Muestra las primeras notas.","Sorts the item by (eg. title, updated_time, created_time).":"Ordena los artículos por campo ( ej. título, fecha de actualización, fecha de creación).","Reverses the sorting order.":"Invierte el orden.","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"Muestra unicamente los artículos de los tipos especificados. Pueden ser `n` para notas, `t` para tareas, o `nt` para libretas y tareas (ej. `-tt` mostrará unicamente las tareas, mientras `-ttd` mostrará notas y tareas).","Either \"text\" or \"json\"":"Puede ser \"text\" o \"json\"","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"Usar formato largo de lista. El formato es ID, NOTE_COUNT ( para libretas), DATE,TODO_CHECKED ( para tareas), TITLE","Please select a notebook first.":"Por favor selecciona la libreta.","Creates a new notebook.":"Crea una nueva libreta.","Creates a new note.":"Crea una nueva nota.","Notes can only be created within a notebook.":"Notas solamente pueden ser creadas dentro de una libreta.","Creates a new to-do.":"Crea una nueva lista de tareas.","Moves the notes matching to [notebook].":"Mueve las notas que coincidan con para la [libreta].","Renames the given (note or notebook) to .":"Renombre el artículo dado (nota o libreta) a .","Deletes the given notebook.":"Elimina la libreta dada.","Deletes the notebook without asking for confirmation.":"Elimina una libreta sin pedir confirmación.","Delete notebook? All notes within this notebook will also be deleted.":"","Deletes the notes matching .":"Elimina las notas que coinciden con .","Deletes the notes without asking for confirmation.":"Elimina las notas sin pedir confirmación.","%d notes match this pattern. Delete them?":"%d notas coinciden con el patron. Eliminarlas?","Delete note?":"Eliminar nota?","Searches for the given in all the notes.":"Buscar el patron en todas las notas.","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"Sets the property of the given to the given [value]. Possible properties are:\n\n%s","Displays summary about the notes and notebooks.":"Muestra un resumen acerca de las notas y las libretas.","Synchronises with remote storage.":"Sincronizar con almacenamiento remoto.","Sync to provided target (defaults to sync.target config value)":"Sincronizar con objetivo proveído ( por defecto al valor de configuración sync.target)","Synchronisation is already in progress.":"Sincronzación en progreso.","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.","Authentication was not completed (did not receive an authentication token).":"Autenticación no completada (no se recibió token de autenticación).","Synchronisation target: %s (%s)":"Objetivo de sincronización: %s (%s)","Cannot initialize synchroniser.":"No se puede inicializar sincronizador.","Starting synchronisation...":"Iniciando sincronización...","Cancelling... Please wait.":"Cancelando... Por favor espere."," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":" puede ser \"add\", \"remove\" o \"list\" para asignar o eliminar [tag] de [note], o para listar las notas asociadas con [tag]. El comando `tag list` puede ser usado para listar todas las etiquetas.","Invalid command: \"%s\"":"Comando inválido: \"%s\""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":" puede ser \"toggle\" o \"clear\". Usa \"toggle\" para cambiar la tarea dada entre estado completado y sin completar. ( Si el objetivo es una nota regular se convertirá en una tarea). Usa \"clear\" para convertir la tarea a una nota regular. ","Marks a to-do as non-completed.":"Marcar una tarea como no completada.","Switches to [notebook] - all further operations will happen within this notebook.":"Cambia una [libreta] - todas las demás operaciones se realizan en ésta libreta.","Displays version information":"Muestra información de la versión","%s %s (%s)":"%s %s (%s)","Enum":"Enumerar","Type: %s.":"Tipo: %s.","Possible values: %s.":"Posibles valores: %s.","Default: %s":"Por defecto: %s","Possible keys/values:":"Teclas/valores posbiles:","Fatal error:":"Error fatal:","The application has been authorised - you may now close this browser tab.":"La aplicación ha sido autorizada - ahora puedes cerrar esta pestaña de tu navegador.","The application has been successfully authorised.":"La aplicacion ha sido autorizada exitosamente.","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.","Search:":"Bucar:","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"","File":"Archivo","New note":"Nueva nota","New to-do":"Nueva lista de tareas","New notebook":"Nueva libreta","Import Evernote notes":"Importar notas de Evernote","Evernote Export Files":"Exportar archivos de Evernote","Quit":"Salir","Edit":"Editar","Copy":"Copiar","Cut":"Cortar","Paste":"Pegar","Search in all the notes":"Buscar en todas las notas","Tools":"Herramientas","Synchronisation status":"Synchronisation status","Options":"Opciones","Help":"Ayuda","Website and documentation":"Sitio web y documentacion","About Joplin":"Acerca de Joplin","%s %s (%s, %s)":"%s %s (%s, %s)","OK":"Ok","Cancel":"Cancelar","Notes and settings are stored in: %s":"","Save":"","Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?":"","Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.":"","Disable encryption":"","Enable encryption":"","Master Keys":"","Active":"","ID":"","Source":"","Created":"Created","Updated":"Updated","Password":"","Password OK":"","Note: Only one master key is going to be used for encryption (the one marked as \"active\"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.":"","Status":"Estatus","Encryption is:":"","Enabled":"Enabled","Disabled":"Deshabilitado","Back":"Back","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"New notebook \"%s\" will be created and file \"%s\" will be imported into it","Please create a notebook first.":"Por favor crea una libreta primero.","Note title:":"Título de nota:","Please create a notebook first":"Por favor crea una libreta primero","To-do title:":"Títuto de lista de tareas:","Notebook title:":"Título de libreta:","Add or remove tags:":"Agregar o borrar etiquetas: ","Separate each tag by a comma.":"Separar cada etiqueta por una coma.","Rename notebook:":"Renombrar libreta:","Set alarm:":"Ajustar alarma:","Layout":"Diseño","Some items cannot be synchronised.":"Some items cannot be synchronised.","View them now":"","Some items cannot be decrypted.":"Some items cannot be decrypted.","Set the password":"","Add or remove tags":"Agregar o borrar etiquetas","Switch between note and to-do type":"Switch between note and to-do type","Delete":"Eliminar","Delete notes?":"Eliminar notas?","No notes in here. Create one by clicking on \"New note\".":"No hay notas aqui. Crea una dando click en \"Nueva nota\".","There is currently no notebook. Create one by clicking on \"New notebook\".":"There is currently no notebook. Create one by clicking on \"New notebook\".","Unsupported link or message: %s":"Enlace o mensaje sin soporte: %s","Attach file":"Adjuntar archivo","Set alarm":"Ajustar alarma","Refresh":"Refrescar","Clear":"Limpiar","OneDrive Login":"Inicio de sesión de OneDrive","Import":"Importar","Synchronisation Status":"Synchronisation Status","Encryption Options":"","Remove this tag from all the notes?":"Remover esta etiqueta de todas las notas?","Remove this search from the sidebar?":"Remover esta busqueda de la barra lateral?","Rename":"Renombrar","Synchronise":"Sincronizar","Notebooks":"Libretas","Tags":"Etiquetas","Searches":"Busquedas","Please select where the sync status should be exported to":"Please select where the sync status should be exported to","Usage: %s":"Uso: %s","Unknown flag: %s":"Etiqueta desconocida: %s","File system":"Sistema de archivos","OneDrive":"OneDrive","OneDrive Dev (For testing only)":"OneDrive Dev (For testing only)","Unknown log level: %s":"Nivel de log desconocido: %s","Unknown level ID: %s":"Nivel de ID desconocido: %s","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.","Cannot access %s":"Cannot access %s","Created local items: %d.":"Artículos locales creados: %d.","Updated local items: %d.":"Artículos locales actualizados: %d.","Created remote items: %d.":"Artículos remotos creados: %d.","Updated remote items: %d.":"Artículos remotos actualizados: %d.","Deleted local items: %d.":"Artículos locales borrados: %d.","Deleted remote items: %d.":"Artículos remotos borrados: %d.","State: \"%s\".":"Estado: \"%s\".","Cancelling...":"Cancelando....","Completed: %s":"Completado: %s","Synchronisation is already in progress. State: %s":"La sincronizacion ya esta en progreso. Estod: %s","Conflicts":"Conflictos","A notebook with this title already exists: \"%s\"":"Ya existe una libreta con este nombre: \"%s\"","Notebooks cannot be named \"%s\", which is a reserved title.":"Notebooks cannot be named \"%s\", which is a reserved title.","Untitled":"Intitulado","This note does not have geolocation information.":"Esta nota no tiene informacion de geolocalización.","Cannot copy note to \"%s\" notebook":"Cannot copy note to \"%s\" notebook","Cannot move note to \"%s\" notebook":"Cannot move note to \"%s\" notebook","Text editor":"Editor de texto","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.","Language":"Lenguaje","Date format":"Formato de fecha","Time format":"Formato de hora","Theme":"Tema","Light":"Claro","Dark":"Oscuro","Show uncompleted todos on top of the lists":"Show uncompleted todos on top of the lists","Save geo-location with notes":"Guardar notas con geo-licalización","Synchronisation interval":"Intervalo de sincronización","%d minutes":"%d minutos","%d hour":"%d hora","%d hours":"%d horas","Automatically update the application":"Actualizacion automatica de la aplicación","Show advanced options":"Mostrar opciones ","Synchronisation target":"Sincronización de objetivo","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.","Directory to synchronise with (absolute path)":"","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"La ubicacion para sincronizar cuando el sistema de archivo tenga habilitada la sincronización. Ver `sync.target`.","Invalid option value: \"%s\". Possible values are: %s.":"Valor inválido de opción: \"%s\". Los válores inválidos son: %s.","Items that cannot be synchronised":"","\"%s\": \"%s\"":"","Sync status (synced items / total items)":"Estatus de sincronización (artículos sincronizados / total de artículos)","%s: %d/%d":"%s: %d/%d","Total: %d/%d":"Total: %d/%d","Conflicted: %d":"Conflictivo: %d","To delete: %d":"Borrar: %d","Folders":"Directorios","%s: %d notes":"%s: %d notas","Coming alarms":"Coming alarms","On %s: %s":"En %s: %s","There are currently no notes. Create one by clicking on the (+) button.":"There are currently no notes. Create one by clicking on the (+) button.","Delete these notes?":"Borrar estas notas?","Log":"Log","Export Debug Report":"Export Debug Report","Configuration":"Configuracion","Move to notebook...":"Mover a libreta....","Move %d notes to notebook \"%s\"?":"Mover %d notas a libreta \"%s\"?","Select date":"Seleccionar fecha","Confirm":"Confirmar","Cancel synchronisation":"Sincronizacion cancelada","The notebook could not be saved: %s":"Esta libreta no pudo ser guardada: %s","Edit notebook":"Editar libreta","This note has been modified:":"Esta nota ha sido modificada:","Save changes":"Guardar cambios","Discard changes":"Descartar cambios","Unsupported image type: %s":"Tipo de imagen no soportado: %s","Attach photo":"Adjuntar foto","Attach any file":"Adjuntar cualquier archivo","Convert to note":"Convertir a nota","Convert to todo":"Convertir a lista de tareas","Hide metadata":"Ocultar metadata","Show metadata":"Mostrar metadata","View on map":"Ver un mapa","Delete notebook":"Borrar libreta","Login with OneDrive":"Loguear con OneDrive","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.","You currently have no notebook. Create one by clicking on (+) button.":"You currently have no notebook. Create one by clicking on (+) button.","Welcome":"Bienvenido"} \ No newline at end of file diff --git a/ElectronClient/app/locales/es_ES.json b/ElectronClient/app/locales/es_ES.json new file mode 100644 index 000000000..80d8238b3 --- /dev/null +++ b/ElectronClient/app/locales/es_ES.json @@ -0,0 +1 @@ +{"Give focus to next pane":"Enfocar el siguiente panel","Give focus to previous pane":"Enfocar el panel anterior","Enter command line mode":"Entrar en modo línea de comandos","Exit command line mode":"Salir del modo línea de comandos","Edit the selected note":"Editar la nota seleccionada","Cancel the current command.":"Cancelar el comando actual.","Exit the application.":"Salir de la aplicación.","Delete the currently selected note or notebook.":"Eliminar la nota o libreta seleccionada.","To delete a tag, untag the associated notes.":"Desmarque las notas asociadas para eliminar una etiqueta.","Please select the note or notebook to be deleted first.":"Seleccione primero la nota o libreta que desea eliminar.","Set a to-do as completed / not completed":"Marca una tarea como completada/no completada","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"in[t]ercambia la [c]onsola entre maximizada/minimizada/oculta/visible.","Search":"Buscar","[t]oggle note [m]etadata.":"in[t]ercambia los [m]etadatos de una nota.","[M]ake a new [n]ote":"[C]rear una [n]ota nueva","[M]ake a new [t]odo":"[C]rear una [t]area nueva","[M]ake a new note[b]ook":"[C]rear una li[b]reta nueva","Copy ([Y]ank) the [n]ote to a notebook.":"Copiar ([Y]ank) la [n]ota a una libreta.","Move the note to a notebook.":"Mover la nota a una libreta.","Press Ctrl+D or type \"exit\" to exit the application":"Pulse Ctrl+D o escriba «salir» para salir de la aplicación","More than one item match \"%s\". Please narrow down your query.":"Hay más de un elemento que coincide con «%s», intente mejorar su consulta.","No notebook selected.":"No se ha seleccionado ninguna libreta.","No notebook has been specified.":"Ninguna libre fue especificada","Y":"Y","n":"n","N":"N","y":"y","Cancelling background synchronisation... Please wait.":"Cancelando sincronización de segundo plano... Por favor espere.","No such command: %s":"El comando no existe: %s","The command \"%s\" is only available in GUI mode":"El comando «%s» solamente está disponible en modo GUI","Missing required argument: %s":"Falta un argumento requerido: %s","%s: %s":"%s: %s","Your choice: ":"Tu elección: ","Invalid answer: %s":"Respuesta inválida: %s","Attaches the given file to the note.":"Adjuntar archivo a la nota.","Cannot find \"%s\".":"No se encuentra \"%s\".","Displays the given note.":"Mostrar la nota dada.","Displays the complete information about note.":"Mostrar la información completa acerca de la nota.","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"Obtener o configurar un valor. Si no se provee el [valor], se mostrará el valor de [nombre]. Si no se provee [nombre] ni [valor], se listara la configuración actual.","Also displays unset and hidden config variables.":"También muestra variables ocultas o no configuradas.","%s = %s (%s)":"%s = %s (%s)","%s = %s":"%s = %s","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"Duplica las notas que coincidan con en la libreta. Si no se especifica una libreta la nota se duplica en la libreta actual.","Marks a to-do as done.":"Marca una tarea como hecha.","Note is not a to-do: \"%s\"":"Una nota no es una tarea: \"%s\"","Edit note.":"Editar una nota.","No text editor is defined. Please set it using `config editor `":"No hay editor de texto definido. Por favor configure uno usando `config editor `","No active notebook.":"No hay libreta activa.","Note does not exist: \"%s\". Create it?":"La nota no existe: \"%s\". Crearla?","Starting to edit note. Close the editor to get back to the prompt.":"Iniciando a editar una nota. Cierra el editor para regresar al prompt.","Note has been saved.":"La nota a sido guardada.","Exits the application.":"Sale de la aplicación.","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"Exportar datos de Joplin al directorio indicado. Por defecto, se exportará la base de datos completa incluyendo libretas, notas, etiquetas y recursos.","Exports only the given note.":"Exportar unicamente la nota indicada.","Exports only the given notebook.":"Exportar unicamente la libreta indicada.","Displays a geolocation URL for the note.":"Mostrar geolocalización de la URL para la nota.","Displays usage information.":"Muestra información de uso.","Shortcuts are not available in CLI mode.":"Atajos no disponibles en modo CLI.","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"Escriba `help [command]` para obtener más información sobre el comando, o escriba `help all` para obtener toda la información acerca del uso del programa.","The possible commands are:":"Los posibles comandos son:","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"Con cualquier comando, una nota o libreta puede ser referida por titulo o ID, o utilizando atajos `$n` o `$b`, respectivamente, para la nota o libreta seleccionada se puede usar `$c` para hacer referencia al artículo seleccionado.","To move from one pane to another, press Tab or Shift+Tab.":"Para mover desde un panel a otro, presiona Tab o Shift+Tab.","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"Para desplazar en las listas y areas de texto ( incluyendo la consola ) utilice las flechas y re pág/av pág.","To maximise/minimise the console, press \"TC\".":"Para maximizar/minimizar la consola, presiona \"TC\".","To enter command line mode, press \":\"":"Para entrar a modo linea de comando, presiona \":\"","To exit command line mode, press ESCAPE":"Para salir de modo linea de comando, presiona ESCAPE","For the complete list of available keyboard shortcuts, type `help shortcuts`":"Para una lista completa de los atajos de teclado disponibles, escribe `help shortcuts`","Imports an Evernote notebook file (.enex file).":"Importar una libreta de Evernote (archivo .enex).","Do not ask for confirmation.":"No preguntar por confirmación.","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"El archivo \"%s\" será importado dentro de la libreta existente \"%s\". Continuar?","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"Nueva libreta \"%s\" será creada y el archivo \"%s\" será importado dentro de ella. Continuar?","Found: %d.":"Encontrado: %d.","Created: %d.":"Creado: %d.","Updated: %d.":"Actualizado: %d.","Skipped: %d.":"Omitido: %d.","Resources: %d.":"Recursos: %d.","Tagged: %d.":"Etiquetado: %d.","Importing notes...":"Importando notas...","The notes have been imported: %s":"Las notas han sido importadas: %s","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"Muestra las notas en la libreta actual. Usa `ls /` para mostrar la lista de libretas.","Displays only the first top notes.":"Muestra las primeras notas.","Sorts the item by (eg. title, updated_time, created_time).":"Ordena los artículos por campo ( ej. título, fecha de actualización, fecha de creación).","Reverses the sorting order.":"Invierte el orden.","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"Muestra unicamente los artículos de los tipos especificados. Pueden ser `n` para notas, `t` para tareas, o `nt` para libretas y tareas (ej. `-tt` mostrará unicamente las tareas, mientras `-ttd` mostrará notas y tareas).","Either \"text\" or \"json\"":"Puede ser \"text\" o \"json\"","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"Usar formato largo de lista. El formato es ID, NOTE_COUNT ( para libretas), DATE,TODO_CHECKED ( para tareas), TITLE","Please select a notebook first.":"Por favor selecciona la libreta.","Creates a new notebook.":"Crea una nueva libreta.","Creates a new note.":"Crea una nueva nota.","Notes can only be created within a notebook.":"Notas solamente pueden ser creadas dentro de una libreta.","Creates a new to-do.":"Crea una nueva lista de tareas.","Moves the notes matching to [notebook].":"Mueve las notas que coincidan con para la [libreta].","Renames the given (note or notebook) to .":"Renombre el artículo dado (nota o libreta) a .","Deletes the given notebook.":"Elimina la libreta dada.","Deletes the notebook without asking for confirmation.":"Elimina una libreta sin pedir confirmación.","Delete notebook? All notes within this notebook will also be deleted.":"¿Desea eliminar la libreta? Todas las notas dentro de esta libreta también serán eliminadas.","Deletes the notes matching .":"Elimina las notas que coinciden con .","Deletes the notes without asking for confirmation.":"Elimina las notas sin pedir confirmación.","%d notes match this pattern. Delete them?":"%d notas coinciden con el patron. Eliminarlas?","Delete note?":"Eliminar nota?","Searches for the given in all the notes.":"Buscar el patron en todas las notas.","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"Asigna el valor [value] a la propiedad de la nota indicada . Propiedades disponibles:\n\n%s","Displays summary about the notes and notebooks.":"Muestra un resumen acerca de las notas y las libretas.","Synchronises with remote storage.":"Sincronizar con almacenamiento remoto.","Sync to provided target (defaults to sync.target config value)":"Sincronizar con objetivo proveído ( por defecto al valor de configuración sync.target)","Synchronisation is already in progress.":"Sincronzación en progreso.","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"Ya hay un archivo de bloqueo. Si está seguro de que no hay una sincronización en curso puede eliminar el archivo de bloqueo «%s» y reanudar la operación.","Authentication was not completed (did not receive an authentication token).":"Autenticación no completada (no se recibió token de autenticación).","Synchronisation target: %s (%s)":"Objetivo de sincronización: %s (%s)","Cannot initialize synchroniser.":"No se puede inicializar sincronizador.","Starting synchronisation...":"Iniciando sincronización...","Cancelling... Please wait.":"Cancelando... Por favor espere."," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":" puede ser \"add\", \"remove\" o \"list\" para asignar o eliminar [tag] de [note], o para listar las notas asociadas con [tag]. El comando `tag list` puede ser usado para listar todas las etiquetas.","Invalid command: \"%s\"":"Comando inválido: \"%s\""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":" puede ser \"toggle\" o \"clear\". Usa \"toggle\" para cambiar la tarea dada entre estado completado y sin completar. ( Si el objetivo es una nota regular se convertirá en una tarea). Usa \"clear\" para convertir la tarea a una nota regular. ","Marks a to-do as non-completed.":"Marcar una tarea como no completada.","Switches to [notebook] - all further operations will happen within this notebook.":"Cambia una [libreta] - todas las demás operaciones se realizan en ésta libreta.","Displays version information":"Muestra información de la versión","%s %s (%s)":"%s %s (%s)","Enum":"Enumerar","Type: %s.":"Tipo: %s.","Possible values: %s.":"Posibles valores: %s.","Default: %s":"Por defecto: %s","Possible keys/values:":"Teclas/valores posbiles:","Fatal error:":"Error fatal:","The application has been authorised - you may now close this browser tab.":"La aplicación ha sido autorizada - ahora puedes cerrar esta pestaña de tu navegador.","The application has been successfully authorised.":"La aplicacion ha sido autorizada exitosamente.","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"Abra la siguiente URL en su navegador para autenticar la aplicación. La aplicación creará un directorio en «Apps/Joplin» y solo leerá y escribirá archivos en este directorio. No tendrá acceso a ningún archivo fuera de este directorio ni a ningún otro archivo personal. No se compartirá información con terceros.","Search:":"Buscar:","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"Bienvenido a Joplin.\n\nEscriba «:help shortcuts» para obtener una lista con los atajos de teclado, o simplemente «:help» para información general.\n\nPor ejemplo, para crear una libreta escriba «mb», para crear una nota escriba «mn».","File":"Archivo","New note":"Nota nueva","New to-do":"Lista de tareas nueva","New notebook":"Libreta nueva","Import Evernote notes":"Importar notas de Evernote","Evernote Export Files":"Archivos exportados de Evernote","Quit":"Salir","Edit":"Editar","Copy":"Copiar","Cut":"Cortar","Paste":"Pegar","Search in all the notes":"Buscar en todas las notas","Tools":"Herramientas","Synchronisation status":"Estado de la sincronización","Options":"Opciones","Help":"Ayuda","Website and documentation":"Sitio web y documentación","About Joplin":"Acerca de Joplin","%s %s (%s, %s)":"%s %s (%s, %s)","OK":"OK","Cancel":"Cancelar","Notes and settings are stored in: %s":"Las notas y los ajustes se guardan en: %s","Save":"Guardar","Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?":"","Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.":"","Disable encryption":"","Enable encryption":"","Master Keys":"","Active":"","ID":"ID","Source":"Origen","Created":"Creado","Updated":"Actualizado","Password":"","Password OK":"","Note: Only one master key is going to be used for encryption (the one marked as \"active\"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.":"","Status":"Estado","Encryption is:":"","Enabled":"Enabled","Disabled":"Deshabilitado","Back":"Atrás","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"Se creará la nueva libreta «%s» y se importará en ella el archivo «%s»","Please create a notebook first.":"Cree primero una libreta.","Note title:":"Título de la nota:","Please create a notebook first":"Por favor crea una libreta primero","To-do title:":"Títuto de lista de tareas:","Notebook title:":"Título de libreta:","Add or remove tags:":"Agregar o borrar etiquetas: ","Separate each tag by a comma.":"Separar cada etiqueta por una coma.","Rename notebook:":"Renombrar libreta:","Set alarm:":"Ajustar alarma:","Layout":"Diseño","Some items cannot be synchronised.":"No se han podido sincronizar algunos de los elementos.","View them now":"Verlos ahora","Some items cannot be decrypted.":"Some items cannot be decrypted.","Set the password":"","Add or remove tags":"Añadir o borrar etiquetas","Switch between note and to-do type":"Cambiar entre nota y lista de tareas","Delete":"Eliminar","Delete notes?":"¿Desea eliminar notas?","No notes in here. Create one by clicking on \"New note\".":"No hay ninguna nota. Cree una pulsando «Nota nueva».","There is currently no notebook. Create one by clicking on \"New notebook\".":"No hay ninguna libreta. Cree una pulsando en «Libreta nueva».","Unsupported link or message: %s":"Enlace o mensaje no soportado: %s","Attach file":"Adjuntar archivo","Set alarm":"Fijar alarma","Refresh":"Refrescar","Clear":"Limpiar","OneDrive Login":"Inicio de sesión de OneDrive","Import":"Importar","Synchronisation Status":"Estado de la sincronización","Encryption Options":"","Remove this tag from all the notes?":"¿Desea eliminar esta etiqueta de todas las notas?","Remove this search from the sidebar?":"¿Desea eliminar esta búsqueda de la barra lateral?","Rename":"Renombrar","Synchronise":"Sincronizar","Notebooks":"Libretas","Tags":"Etiquetas","Searches":"Búsquedas","Please select where the sync status should be exported to":"Seleccione a dónde se debería exportar el estado de sincronización","Usage: %s":"Uso: %s","Unknown flag: %s":"Etiqueta desconocida: %s","File system":"Sistema de archivos","OneDrive":"OneDrive","OneDrive Dev (For testing only)":"OneDrive Dev (Solo para pruebas)","Unknown log level: %s":"Nivel de log desconocido: %s","Unknown level ID: %s":"ID de nivel desconocido: %s","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"No se ha podido actualizar token: faltan datos de autenticación. Reiniciar la sincronización podría solucionar el problema.","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"No se ha podido sincronizar con OneDrive.\n\nEste error suele ocurrir al utilizar OneDrive for Business. Este producto no está soportado.\n\nPodría considerar utilizar una cuenta Personal de OneDrive.","Cannot access %s":"No se ha podido acceder a %s","Created local items: %d.":"Elementos locales creados: %d.","Updated local items: %d.":"Elementos locales actualizados: %d.","Created remote items: %d.":"Elementos remotos creados: %d.","Updated remote items: %d.":"Elementos remotos actualizados: %d.","Deleted local items: %d.":"Elementos locales borrados: %d.","Deleted remote items: %d.":"Elementos remotos borrados: %d.","State: \"%s\".":"Estado: «%s».","Cancelling...":"Cancelando...","Completed: %s":"Completado: %s","Synchronisation is already in progress. State: %s":"La sincronización ya está en progreso. Estado: %s","Conflicts":"Conflictos","A notebook with this title already exists: \"%s\"":"Ya existe una libreta con este nombre: «%s»","Notebooks cannot be named \"%s\", which is a reserved title.":"No se puede usar el nombre «%s» para una libreta; es un título reservado.","Untitled":"Sin título","This note does not have geolocation information.":"Esta nota no tiene informacion de geolocalización.","Cannot copy note to \"%s\" notebook":"No se ha podido copiar la nota a la libreta «%s»","Cannot move note to \"%s\" notebook":"No se ha podido mover la nota a la libreta «%s»","Text editor":"Editor de texto","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"El editor que se usará para abrir una nota. Se intentará auto-detectar el editor predeterminado si no se proporciona ninguno.","Language":"Idioma","Date format":"Formato de fecha","Time format":"Formato de hora","Theme":"Tema","Light":"Claro","Dark":"Oscuro","Show uncompleted todos on top of the lists":"Mostrar tareas incompletas al inicio de las listas","Save geo-location with notes":"Guardar geolocalización en las notas","Synchronisation interval":"Intervalo de sincronización","%d minutes":"%d minutos","%d hour":"%d hora","%d hours":"%d horas","Automatically update the application":"Actualizar la aplicación automáticamente","Show advanced options":"Mostrar opciones avanzadas","Synchronisation target":"Destino de sincronización","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"El destino de la sincronización. Si se sincroniza con el sistema de archivos, indique el directorio destino en «sync.2.path».","Directory to synchronise with (absolute path)":"Directorio con el que sincronizarse (ruta completa)","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"La ruta a la que sincronizar cuando se activa la sincronización con sistema de archivos. Vea «sync.target».","Invalid option value: \"%s\". Possible values are: %s.":"Opción inválida: «%s». Los valores posibles son: %s.","Items that cannot be synchronised":"Elementos que no se pueden sincronizar","\"%s\": \"%s\"":"«%s»: «%s»","Sync status (synced items / total items)":"Estado de sincronización (elementos sincronizados/elementos totales)","%s: %d/%d":"%s: %d/%d","Total: %d/%d":"Total: %d/%d","Conflicted: %d":"Conflictos: %d","To delete: %d":"Borrar: %d","Folders":"Directorios","%s: %d notes":"%s: %d notas","Coming alarms":"Alarmas inminentes","On %s: %s":"En %s: %s","There are currently no notes. Create one by clicking on the (+) button.":"No hay notas. Cree una pulsando en el botón (+).","Delete these notes?":"¿Desea borrar estas notas?","Log":"Log","Export Debug Report":"Exportar informe de depuración","Configuration":"Configuración","Move to notebook...":"Mover a la libreta...","Move %d notes to notebook \"%s\"?":"¿Desea mover %d notas a libreta «%s»?","Select date":"Seleccione fecha","Confirm":"Confirmar","Cancel synchronisation":"Cancelar sincronización","The notebook could not be saved: %s":"No se ha podido guardar esta libreta: %s","Edit notebook":"Editar libreta","This note has been modified:":"Esta nota ha sido modificada:","Save changes":"Guardar cambios","Discard changes":"Descartar cambios","Unsupported image type: %s":"Tipo de imagen no soportado: %s","Attach photo":"Adjuntar foto","Attach any file":"Adjuntar cualquier archivo","Convert to note":"Convertir a nota","Convert to todo":"Convertir a lista de tareas","Hide metadata":"Ocultar metadatos","Show metadata":"Mostrar metadatos","View on map":"Ver en un mapa","Delete notebook":"Borrar libreta","Login with OneDrive":"Acceder con OneDrive","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"Pulse en el botón (+) para crear una nueva nota o libreta. Pulse en el menú lateral para acceder a las libretas existentes.","You currently have no notebook. Create one by clicking on (+) button.":"No hay ninguna libreta. Cree una nueva libreta pulsando en el botón (+).","Welcome":"Bienvenido"} \ No newline at end of file diff --git a/ElectronClient/app/locales/fr_FR.json b/ElectronClient/app/locales/fr_FR.json index e6a01c340..198223917 100644 --- a/ElectronClient/app/locales/fr_FR.json +++ b/ElectronClient/app/locales/fr_FR.json @@ -1 +1 @@ -{"Give focus to next pane":"Activer le volet suivant","Give focus to previous pane":"Activer le volet précédent","Enter command line mode":"Démarrer le mode de ligne de commande","Exit command line mode":"Sortir du mode de ligne de commande","Edit the selected note":"Éditer la note sélectionnée","Cancel the current command.":"Annuler la commande en cours.","Exit the application.":"Quitter le logiciel.","Delete the currently selected note or notebook.":"Supprimer la note ou carnet sélectionné.","To delete a tag, untag the associated notes.":"Pour supprimer une vignette, enlever là des notes associées.","Please select the note or notebook to be deleted first.":"Veuillez d'abord sélectionner un carnet.","Set a to-do as completed / not completed":"Marquer une tâches comme complétée / non-complétée","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"Maximiser, minimiser, cacher ou rendre visible la console.","Search":"Chercher","[t]oggle note [m]etadata.":"Afficher/Cacher les métadonnées des notes.","[M]ake a new [n]ote":"Créer une nouvelle note","[M]ake a new [t]odo":"Créer une nouvelle tâche","[M]ake a new note[b]ook":"Créer un nouveau carnet","Copy ([Y]ank) the [n]ote to a notebook.":"Copier la note dans un autre carnet.","Move the note to a notebook.":"Déplacer la note vers un carnet.","Press Ctrl+D or type \"exit\" to exit the application":"Appuyez sur Ctrl+D ou tapez \"exit\" pour sortir du logiciel","More than one item match \"%s\". Please narrow down your query.":"Plus d'un objet correspond à \"%s\". Veuillez préciser votre requête.","No notebook selected.":"Aucun carnet n'est sélectionné.","No notebook has been specified.":"Aucun carnet n'est spécifié.","Y":"O","n":"n","N":"N","y":"o","Cancelling background synchronisation... Please wait.":"Annulation de la synchronisation... Veuillez patienter.","No such command: %s":"No such command: %s","The command \"%s\" is only available in GUI mode":"La commande \"%s\" est disponible uniquement en mode d'interface graphique","Missing required argument: %s":"Paramètre requis manquant : %s","%s: %s":"%s : %s","Your choice: ":"Votre choix : ","Invalid answer: %s":"Réponse invalide : %s","Attaches the given file to the note.":"Joindre le fichier fourni à la note.","Cannot find \"%s\".":"Impossible de trouver \"%s\".","Displays the given note.":"Affiche la note.","Displays the complete information about note.":"Affiche tous les détails de la note.","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"Obtient ou modifie une valeur de configuration. Si la [valeur] n'est pas fournie, la valeur de [nom] est affichée. Si ni le [nom] ni la [valeur] ne sont fournis, la configuration complète est affichée.","Also displays unset and hidden config variables.":"Afficher également les variables cachées.","%s = %s (%s)":"%s = %s (%s)","%s = %s":"%s = %s","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"Copier les notes correspondant à vers [carnet]. Si aucun carnet n'est spécifié, la note est dupliquée sur place.","Marks a to-do as done.":"Marquer la tâche comme complétée.","Note is not a to-do: \"%s\"":"La note n'est pas une tâche : \"%s\"","Edit note.":"Éditer la note.","No text editor is defined. Please set it using `config editor `":"Aucun éditeur de texte n'est défini. Veuillez le définir en utilisant la commande `config editor `","No active notebook.":"Aucun carnet actif.","Note does not exist: \"%s\". Create it?":"Cette note n'existe pas : \"%s\". La créer ?","Starting to edit note. Close the editor to get back to the prompt.":"Édition de la note en cours. Fermez l'éditeur de texte pour retourner à l'invite de commande.","Note has been saved.":"La note a été enregistrée.","Exits the application.":"Quitter le logiciel.","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"Exporter les données de Joplin vers le dossier fourni. Par défaut, la base de donnée complète sera exportée, y compris les carnets, notes, tags et resources.","Exports only the given note.":"Exporter uniquement la note spécifiée.","Exports only the given notebook.":"Exporter uniquement le carnet spécifié.","Displays a geolocation URL for the note.":"Afficher l'URL de l'emplacement de la note.","Displays usage information.":"Affiche les informations d'utilisation.","Shortcuts are not available in CLI mode.":"Les raccourcis ne sont pas disponible en mode de ligne de commande.","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.","The possible commands are:":"Les commandes possibles sont :","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"Dans une commande, une note ou carnet peut être référé par titre ou identifiant, ou en utilisant les raccourcis `$n` et `$b` pour, respectivement, la note sélectionnée et le carnet sélectionné. `$c` peut être utilisé pour faire référence à l'objet sélectionné en cours.","To move from one pane to another, press Tab or Shift+Tab.":"Pour aller d'un volet à l'autre, pressez Tab ou Maj+Tab.","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"Utilisez les touches fléchées et page précédente/suivante pour faire défiler les listes et zones de texte (y compris cette console).","To maximise/minimise the console, press \"TC\".":"Pour maximiser ou minimiser la console, pressez \"TC\".","To enter command line mode, press \":\"":"Pour démarrer le mode ligne de commande, pressez \":\"","To exit command line mode, press ESCAPE":"Pour sortir du mode ligne de commande, pressez ECHAP","For the complete list of available keyboard shortcuts, type `help shortcuts`":"Pour la liste complète des raccourcis disponibles, tapez `help shortcuts`","Imports an Evernote notebook file (.enex file).":"Importer un carnet Evernote (fichier .enex).","Do not ask for confirmation.":"Ne pas demander de confirmation.","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"Le fichier \"%s\" va être importé dans le carnet existant \"%s\". Continuer ?","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"Un nouveau carnet \"%s\" va être créé et le fichier \"%s\" va être importé dedans. Continuer ?","Found: %d.":"Trouvés : %d.","Created: %d.":"Créés : %d.","Updated: %d.":"Mis à jour : %d.","Skipped: %d.":"Ignorés : %d.","Resources: %d.":"Ressources : %d.","Tagged: %d.":"Étiquettes : %d.","Importing notes...":"Importation des notes...","The notes have been imported: %s":"Les notes ont été importées : %s","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"Affiche les notes dans le carnet. Utilisez `ls /` pour afficher la liste des carnets.","Displays only the first top notes.":"Affiche uniquement les premières notes.","Sorts the item by (eg. title, updated_time, created_time).":"Trier les notes par (par exemple, title, updated_time, created_time).","Reverses the sorting order.":"Inverser l'ordre.","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"Affiche uniquement les notes du ou des types spécifiés. Le type peut-être `n` pour les notes, `t` pour les tâches (par exemple, `-tt` affiche uniquement les tâches, tandis que `-ttd` affiche les notes et les tâches).","Either \"text\" or \"json\"":"Soit \"text\" soit \"json\"","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"Utilise le format de liste longue. Le format est ID, NOMBRE_DE_NOTES (pour les carnets), DATE, TACHE_TERMINE (pour les tâches), TITRE","Please select a notebook first.":"Veuillez d'abord sélectionner un carnet.","Creates a new notebook.":"Créer un carnet.","Creates a new note.":"Créer une note.","Notes can only be created within a notebook.":"Les notes ne peuvent être créées que dans un carnet.","Creates a new to-do.":"Créer une nouvelle tâche.","Moves the notes matching to [notebook].":"Déplacer les notes correspondant à vers [notebook].","Renames the given (note or notebook) to .":"Renommer l'objet (note ou carnet) en .","Deletes the given notebook.":"Supprimer le carnet.","Deletes the notebook without asking for confirmation.":"Supprimer le carnet sans demander la confirmation.","Delete notebook? All notes within this notebook will also be deleted.":"","Deletes the notes matching .":"Supprimer les notes correspondants à .","Deletes the notes without asking for confirmation.":"Supprimer les notes sans demander la confirmation.","%d notes match this pattern. Delete them?":"%d notes correspondent à ce motif. Les supprimer ?","Delete note?":"Supprimer la note ?","Searches for the given in all the notes.":"Chercher le motif dans toutes les notes.","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"Sets the property of the given to the given [value]. Possible properties are:\n\n%s","Displays summary about the notes and notebooks.":"Afficher un résumé des notes et carnets.","Synchronises with remote storage.":"Synchroniser les notes et carnets.","Sync to provided target (defaults to sync.target config value)":"Synchroniser avec la cible donnée (par défaut, la valeur de configuration `sync.target`).","Synchronisation is already in progress.":"La synchronisation est déjà en cours.","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"La synchronisation est déjà en cours ou ne s'est pas interrompue correctement. Si vous savez qu'aucune autre synchronisation est en cours, vous pouvez supprimer le fichier \"%s\" pour reprendre l'opération.","Authentication was not completed (did not receive an authentication token).":"Impossible d'autoriser le logiciel (jeton d'identification non-reçu).","Synchronisation target: %s (%s)":"Cible de la synchronisation : %s (%s)","Cannot initialize synchroniser.":"Impossible d'initialiser la synchronisation.","Starting synchronisation...":"Commencement de la synchronisation...","Cancelling... Please wait.":"Annulation... Veuillez attendre."," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":" peut être \"add\", \"remove\" ou \"list\" pour assigner ou enlever l'étiquette [tag] de la [note], our pour lister les notes associées avec l'étiquette [tag]. La commande `tag list` peut être utilisée pour lister les étiquettes.","Invalid command: \"%s\"":"Commande invalide : \"%s\""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":"Gère le status des tâches. peut être \"toggle\" ou \"clear\". Utilisez \"toggle\" pour basculer la tâche entre le status terminé et non-terminé (Si la cible est une note, elle sera convertie en tâche). Utilisez \"clear\" pour convertir la tâche en note.","Marks a to-do as non-completed.":"Marquer une tâche comme non-complétée.","Switches to [notebook] - all further operations will happen within this notebook.":"Changer de carnet - toutes les opérations à venir se feront dans ce carnet.","Displays version information":"Affiche les informations de version","%s %s (%s)":"%s %s (%s)","Enum":"Enum","Type: %s.":"Type : %s.","Possible values: %s.":"Valeurs possibles : %s.","Default: %s":"Défaut : %s","Possible keys/values:":"Clefs/Valeurs possibles :","Fatal error:":"Erreur fatale :","The application has been authorised - you may now close this browser tab.":"Le logiciel a été autorisé. Vous pouvez maintenant fermer cet onglet.","The application has been successfully authorised.":"Le logiciel a été autorisé.","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"Veuillez ouvrir le lien ci-dessous dans votre navigateur pour authentifier le logiciel. Joplin va créer un répertoire \"Apps/Joplin\" et lire/écrira des fichiers uniquement dans ce répertoire. Le logiciel n'aura pas d'accès à aucun fichier en dehors de ce répertoire, ni à d'autres données personnelles. Aucune donnée ne sera partagé avec aucun tier.","Search:":"Recherche :","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"","File":"Fichier","New note":"Nouvelle note","New to-do":"Nouvelle tâche","New notebook":"Nouveau carnet","Import Evernote notes":"Importer notes d'Evernote","Evernote Export Files":"Fichiers d'export Evernote","Quit":"Quitter","Edit":"Édition","Copy":"Copier","Cut":"Couper","Paste":"Coller","Search in all the notes":"Chercher dans toutes les notes","Tools":"Outils","Synchronisation status":"Synchronisation status","Options":"Options","Help":"Aide","Website and documentation":"Documentation en ligne","About Joplin":"A propos de Joplin","%s %s (%s, %s)":"%s %s (%s, %s)","OK":"OK","Cancel":"Annulation","Notes and settings are stored in: %s":"","Save":"","Back":"Retour","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"Un nouveau carnet \"%s\" va être créé et le fichier \"%s\" va être importé dedans","Please create a notebook first.":"Veuillez d'abord sélectionner un carnet.","Note title:":"Titre de la note :","Please create a notebook first":"Veuillez d'abord créer un carnet d'abord","To-do title:":"Titre de la tâche :","Notebook title:":"Titre du carnet :","Add or remove tags:":"Modifier les étiquettes :","Separate each tag by a comma.":"Séparez chaque étiquette par une virgule.","Rename notebook:":"Renommer le carnet :","Set alarm:":"Set alarm:","Layout":"Disposition","Some items cannot be synchronised.":"Some items cannot be synchronised.","View them now":"","ID":"","Source":"","Created":"Created","Updated":"Updated","Add or remove tags":"Gérer les étiquettes","Switch between note and to-do type":"Alterner entre note et tâche","Delete":"Supprimer","Delete notes?":"Supprimer les notes ?","No notes in here. Create one by clicking on \"New note\".":"Pas de notes ici. Créez-en une en pressant le bouton \"Nouvelle note\".","There is currently no notebook. Create one by clicking on \"New notebook\".":"There is currently no notebook. Create one by clicking on \"New notebook\".","Unsupported link or message: %s":"Lien ou message non géré : %s","Attach file":"Attacher un fichier","Set alarm":"Set alarm","Refresh":"Rafraîchir","Clear":"Supprimer","OneDrive Login":"Connexion OneDrive","Import":"Importer","Synchronisation Status":"Synchronisation Status","Remove this tag from all the notes?":"Enlever cette étiquette de toutes les notes ?","Remove this search from the sidebar?":"Enlever cette recherche de la barre latérale ?","Rename":"Renommer","Synchronise":"Synchroniser","Notebooks":"Carnets","Tags":"Étiquettes","Searches":"Recherches","Please select where the sync status should be exported to":"Please select where the sync status should be exported to","Usage: %s":"Utilisation : %s","Unknown flag: %s":"Paramètre inconnu : %s","File system":"Système de fichier","OneDrive":"OneDrive","OneDrive Dev (For testing only)":"OneDrive Dév (Pour tester uniquement)","Unknown log level: %s":"Paramètre inconnu : %s","Unknown level ID: %s":"Paramètre inconnu : %s","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"Impossible de rafraîchir la connexion à OneDrive. Démarrez la synchronisation à nouveau pour corriger le problème.","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"","Cannot access %s":"Impossible d'accéder à %s","Created local items: %d.":"Objets créés localement : %d.","Updated local items: %d.":"Objets mis à jour localement : %d.","Created remote items: %d.":"Objets distants créés : %d.","Updated remote items: %d.":"Objets distants mis à jour : %d.","Deleted local items: %d.":"Objets supprimés localement : %d.","Deleted remote items: %d.":"Objets distants supprimés : %d.","State: \"%s\".":"État : \"%s\".","Cancelling...":"Annulation...","Completed: %s":"Terminé : %s","Synchronisation is already in progress. State: %s":"La synchronisation est déjà en cours. État : %s","Conflicts":"Conflits","A notebook with this title already exists: \"%s\"":"Un carnet avec ce titre existe déjà : \"%s\"","Notebooks cannot be named \"%s\", which is a reserved title.":"Les carnets ne peuvent être nommés \"%s\" car c'est un nom réservé.","Untitled":"Sans titre","This note does not have geolocation information.":"Cette note n'a pas d'information d'emplacement.","Cannot copy note to \"%s\" notebook":"Impossible de copier la note vers le carnet \"%s\"","Cannot move note to \"%s\" notebook":"Impossible de déplacer la note vers le carnet \"%s\"","Text editor":"Éditeur de texte","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"L'éditeur de texte pour ouvrir et modifier les notes. Si aucun n'est spécifié, il sera détecté automatiquement.","Language":"Langue","Date format":"Format de la date","Time format":"Format de l'heure","Theme":"Apparence","Light":"Clair","Dark":"Sombre","Show uncompleted todos on top of the lists":"Tâches non-terminées en haut des listes","Save geo-location with notes":"Enregistrer l'emplacement avec les notes","Synchronisation interval":"Intervalle de synchronisation","Disabled":"Désactivé","%d minutes":"%d minutes","%d hour":"%d heure","%d hours":"%d heures","Automatically update the application":"Mettre à jour le logiciel automatiquement","Show advanced options":"Montrer les options avancées","Synchronisation target":"Cible de la synchronisation","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"La cible avec laquelle synchroniser. Pour synchroniser avec le système de fichier, veuillez spécifier le répertoire avec `sync.2.path`.","Directory to synchronise with (absolute path)":"","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"Le chemin du répertoire avec lequel synchroniser lorsque la synchronisation par système de fichier est activée. Voir `sync.target`.","Invalid option value: \"%s\". Possible values are: %s.":"Option invalide: \"%s\". Les valeurs possibles sont : %s.","Items that cannot be synchronised":"","\"%s\": \"%s\"":"","Sync status (synced items / total items)":"Status de la synchronisation (objets synchro. / total)","%s: %d/%d":"%s: %d/%d","Total: %d/%d":"Total : %d/%d","Conflicted: %d":"Conflits : %d","To delete: %d":"A supprimer : %d","Folders":"Carnets","%s: %d notes":"%s : %d notes","Coming alarms":"Alarmes à venir","On %s: %s":"Le %s : %s","There are currently no notes. Create one by clicking on the (+) button.":"Ce carnet ne contient aucune note. Créez-en une en appuyant sur le bouton (+).","Delete these notes?":"Supprimer ces notes ?","Log":"Journal","Status":"État","Export Debug Report":"Exporter rapport de débogage","Configuration":"Configuration","Move to notebook...":"Déplacer la note vers carnet...","Move %d notes to notebook \"%s\"?":"Déplacer %d notes vers carnet \"%s\" ?","Select date":"Sélectionner date","Confirm":"Confirmer","Cancel synchronisation":"Annuler synchronisation","The notebook could not be saved: %s":"Ce carnet n'a pas pu être sauvegardé : %s","Edit notebook":"Éditer le carnet","This note has been modified:":"Cette note a été modifiée :","Save changes":"Enregistrer les changements","Discard changes":"Ignorer les changements","Unsupported image type: %s":"Type d'image non géré : %s","Attach photo":"Attacher une photo","Attach any file":"Attacher un fichier","Convert to note":"Convertir en note","Convert to todo":"Convertir en tâche","Hide metadata":"Cacher les métadonnées","Show metadata":"Afficher les métadonnées","View on map":"Voir emplacement sur carte","Delete notebook":"Supprimer le carnet","Login with OneDrive":"Se connecter à OneDrive","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"Appuyez sur le bouton (+) pour créer une nouvelle note ou carnet. Ouvrez le menu latéral pour accéder à vos carnets.","You currently have no notebook. Create one by clicking on (+) button.":"Vous n'avez pour l'instant pas de carnets. Créez-en un en pressant le bouton (+).","Welcome":"Bienvenue"} \ No newline at end of file +{"Give focus to next pane":"Activer le volet suivant","Give focus to previous pane":"Activer le volet précédent","Enter command line mode":"Démarrer le mode de ligne de commande","Exit command line mode":"Sortir du mode de ligne de commande","Edit the selected note":"Éditer la note sélectionnée","Cancel the current command.":"Annuler la commande en cours.","Exit the application.":"Quitter le logiciel.","Delete the currently selected note or notebook.":"Supprimer la note ou carnet sélectionné.","To delete a tag, untag the associated notes.":"Pour supprimer une vignette, enlever là des notes associées.","Please select the note or notebook to be deleted first.":"Veuillez d'abord sélectionner un carnet.","Set a to-do as completed / not completed":"Marquer une tâches comme complétée / non-complétée","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"Maximiser, minimiser, cacher ou rendre visible la console.","Search":"Chercher","[t]oggle note [m]etadata.":"Afficher/Cacher les métadonnées des notes.","[M]ake a new [n]ote":"Créer une nouvelle note","[M]ake a new [t]odo":"Créer une nouvelle tâche","[M]ake a new note[b]ook":"Créer un nouveau carnet","Copy ([Y]ank) the [n]ote to a notebook.":"Copier la note dans un autre carnet.","Move the note to a notebook.":"Déplacer la note vers un carnet.","Press Ctrl+D or type \"exit\" to exit the application":"Appuyez sur Ctrl+D ou tapez \"exit\" pour sortir du logiciel","More than one item match \"%s\". Please narrow down your query.":"Plus d'un objet correspond à \"%s\". Veuillez préciser votre requête.","No notebook selected.":"Aucun carnet n'est sélectionné.","No notebook has been specified.":"Aucun carnet n'est spécifié.","Y":"O","n":"n","N":"N","y":"o","Cancelling background synchronisation... Please wait.":"Annulation de la synchronisation... Veuillez patienter.","No such command: %s":"No such command: %s","The command \"%s\" is only available in GUI mode":"La commande \"%s\" est disponible uniquement en mode d'interface graphique","Missing required argument: %s":"Paramètre requis manquant : %s","%s: %s":"%s : %s","Your choice: ":"Votre choix : ","Invalid answer: %s":"Réponse invalide : %s","Attaches the given file to the note.":"Joindre le fichier fourni à la note.","Cannot find \"%s\".":"Impossible de trouver \"%s\".","Displays the given note.":"Affiche la note.","Displays the complete information about note.":"Affiche tous les détails de la note.","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"Obtient ou modifie une valeur de configuration. Si la [valeur] n'est pas fournie, la valeur de [nom] est affichée. Si ni le [nom] ni la [valeur] ne sont fournis, la configuration complète est affichée.","Also displays unset and hidden config variables.":"Afficher également les variables cachées.","%s = %s (%s)":"%s = %s (%s)","%s = %s":"%s = %s","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"Copier les notes correspondant à vers [carnet]. Si aucun carnet n'est spécifié, la note est dupliquée sur place.","Marks a to-do as done.":"Marquer la tâche comme complétée.","Note is not a to-do: \"%s\"":"La note n'est pas une tâche : \"%s\"","Edit note.":"Éditer la note.","No text editor is defined. Please set it using `config editor `":"Aucun éditeur de texte n'est défini. Veuillez le définir en utilisant la commande `config editor `","No active notebook.":"Aucun carnet actif.","Note does not exist: \"%s\". Create it?":"Cette note n'existe pas : \"%s\". La créer ?","Starting to edit note. Close the editor to get back to the prompt.":"Édition de la note en cours. Fermez l'éditeur de texte pour retourner à l'invite de commande.","Note has been saved.":"La note a été enregistrée.","Exits the application.":"Quitter le logiciel.","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"Exporter les données de Joplin vers le dossier fourni. Par défaut, la base de donnée complète sera exportée, y compris les carnets, notes, tags et resources.","Exports only the given note.":"Exporter uniquement la note spécifiée.","Exports only the given notebook.":"Exporter uniquement le carnet spécifié.","Displays a geolocation URL for the note.":"Afficher l'URL de l'emplacement de la note.","Displays usage information.":"Affiche les informations d'utilisation.","Shortcuts are not available in CLI mode.":"Les raccourcis ne sont pas disponible en mode de ligne de commande.","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.","The possible commands are:":"Les commandes possibles sont :","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"Dans une commande, une note ou carnet peut être référé par titre ou identifiant, ou en utilisant les raccourcis `$n` et `$b` pour, respectivement, la note sélectionnée et le carnet sélectionné. `$c` peut être utilisé pour faire référence à l'objet sélectionné en cours.","To move from one pane to another, press Tab or Shift+Tab.":"Pour aller d'un volet à l'autre, pressez Tab ou Maj+Tab.","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"Utilisez les touches fléchées et page précédente/suivante pour faire défiler les listes et zones de texte (y compris cette console).","To maximise/minimise the console, press \"TC\".":"Pour maximiser ou minimiser la console, pressez \"TC\".","To enter command line mode, press \":\"":"Pour démarrer le mode ligne de commande, pressez \":\"","To exit command line mode, press ESCAPE":"Pour sortir du mode ligne de commande, pressez ECHAP","For the complete list of available keyboard shortcuts, type `help shortcuts`":"Pour la liste complète des raccourcis disponibles, tapez `help shortcuts`","Imports an Evernote notebook file (.enex file).":"Importer un carnet Evernote (fichier .enex).","Do not ask for confirmation.":"Ne pas demander de confirmation.","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"Le fichier \"%s\" va être importé dans le carnet existant \"%s\". Continuer ?","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"Un nouveau carnet \"%s\" va être créé et le fichier \"%s\" va être importé dedans. Continuer ?","Found: %d.":"Trouvés : %d.","Created: %d.":"Créés : %d.","Updated: %d.":"Mis à jour : %d.","Skipped: %d.":"Ignorés : %d.","Resources: %d.":"Ressources : %d.","Tagged: %d.":"Étiquettes : %d.","Importing notes...":"Importation des notes...","The notes have been imported: %s":"Les notes ont été importées : %s","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"Affiche les notes dans le carnet. Utilisez `ls /` pour afficher la liste des carnets.","Displays only the first top notes.":"Affiche uniquement les premières notes.","Sorts the item by (eg. title, updated_time, created_time).":"Trier les notes par (par exemple, title, updated_time, created_time).","Reverses the sorting order.":"Inverser l'ordre.","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"Affiche uniquement les notes du ou des types spécifiés. Le type peut-être `n` pour les notes, `t` pour les tâches (par exemple, `-tt` affiche uniquement les tâches, tandis que `-ttd` affiche les notes et les tâches).","Either \"text\" or \"json\"":"Soit \"text\" soit \"json\"","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"Utilise le format de liste longue. Le format est ID, NOMBRE_DE_NOTES (pour les carnets), DATE, TACHE_TERMINE (pour les tâches), TITRE","Please select a notebook first.":"Veuillez d'abord sélectionner un carnet.","Creates a new notebook.":"Créer un carnet.","Creates a new note.":"Créer une note.","Notes can only be created within a notebook.":"Les notes ne peuvent être créées que dans un carnet.","Creates a new to-do.":"Créer une nouvelle tâche.","Moves the notes matching to [notebook].":"Déplacer les notes correspondant à vers [notebook].","Renames the given (note or notebook) to .":"Renommer l'objet (note ou carnet) en .","Deletes the given notebook.":"Supprimer le carnet.","Deletes the notebook without asking for confirmation.":"Supprimer le carnet sans demander la confirmation.","Delete notebook? All notes within this notebook will also be deleted.":"","Deletes the notes matching .":"Supprimer les notes correspondants à .","Deletes the notes without asking for confirmation.":"Supprimer les notes sans demander la confirmation.","%d notes match this pattern. Delete them?":"%d notes correspondent à ce motif. Les supprimer ?","Delete note?":"Supprimer la note ?","Searches for the given in all the notes.":"Chercher le motif dans toutes les notes.","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"Sets the property of the given to the given [value]. Possible properties are:\n\n%s","Displays summary about the notes and notebooks.":"Afficher un résumé des notes et carnets.","Synchronises with remote storage.":"Synchroniser les notes et carnets.","Sync to provided target (defaults to sync.target config value)":"Synchroniser avec la cible donnée (par défaut, la valeur de configuration `sync.target`).","Synchronisation is already in progress.":"La synchronisation est déjà en cours.","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"La synchronisation est déjà en cours ou ne s'est pas interrompue correctement. Si vous savez qu'aucune autre synchronisation est en cours, vous pouvez supprimer le fichier \"%s\" pour reprendre l'opération.","Authentication was not completed (did not receive an authentication token).":"Impossible d'autoriser le logiciel (jeton d'identification non-reçu).","Synchronisation target: %s (%s)":"Cible de la synchronisation : %s (%s)","Cannot initialize synchroniser.":"Impossible d'initialiser la synchronisation.","Starting synchronisation...":"Commencement de la synchronisation...","Cancelling... Please wait.":"Annulation... Veuillez attendre."," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":" peut être \"add\", \"remove\" ou \"list\" pour assigner ou enlever l'étiquette [tag] de la [note], our pour lister les notes associées avec l'étiquette [tag]. La commande `tag list` peut être utilisée pour lister les étiquettes.","Invalid command: \"%s\"":"Commande invalide : \"%s\""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":"Gère le status des tâches. peut être \"toggle\" ou \"clear\". Utilisez \"toggle\" pour basculer la tâche entre le status terminé et non-terminé (Si la cible est une note, elle sera convertie en tâche). Utilisez \"clear\" pour convertir la tâche en note.","Marks a to-do as non-completed.":"Marquer une tâche comme non-complétée.","Switches to [notebook] - all further operations will happen within this notebook.":"Changer de carnet - toutes les opérations à venir se feront dans ce carnet.","Displays version information":"Affiche les informations de version","%s %s (%s)":"%s %s (%s)","Enum":"Enum","Type: %s.":"Type : %s.","Possible values: %s.":"Valeurs possibles : %s.","Default: %s":"Défaut : %s","Possible keys/values:":"Clefs/Valeurs possibles :","Fatal error:":"Erreur fatale :","The application has been authorised - you may now close this browser tab.":"Le logiciel a été autorisé. Vous pouvez maintenant fermer cet onglet.","The application has been successfully authorised.":"Le logiciel a été autorisé.","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"Veuillez ouvrir le lien ci-dessous dans votre navigateur pour authentifier le logiciel. Joplin va créer un répertoire \"Apps/Joplin\" et lire/écrira des fichiers uniquement dans ce répertoire. Le logiciel n'aura pas d'accès à aucun fichier en dehors de ce répertoire, ni à d'autres données personnelles. Aucune donnée ne sera partagé avec aucun tier.","Search:":"Recherche :","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"","File":"Fichier","New note":"Nouvelle note","New to-do":"Nouvelle tâche","New notebook":"Nouveau carnet","Import Evernote notes":"Importer notes d'Evernote","Evernote Export Files":"Fichiers d'export Evernote","Quit":"Quitter","Edit":"Édition","Copy":"Copier","Cut":"Couper","Paste":"Coller","Search in all the notes":"Chercher dans toutes les notes","Tools":"Outils","Synchronisation status":"Synchronisation status","Options":"Options","Help":"Aide","Website and documentation":"Documentation en ligne","About Joplin":"A propos de Joplin","%s %s (%s, %s)":"%s %s (%s, %s)","OK":"OK","Cancel":"Annulation","Notes and settings are stored in: %s":"","Save":"","Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?":"","Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.":"","Disable encryption":"","Enable encryption":"","Master Keys":"","Active":"","ID":"","Source":"","Created":"Created","Updated":"Updated","Password":"","Password OK":"","Note: Only one master key is going to be used for encryption (the one marked as \"active\"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.":"","Status":"État","Encryption is:":"","Enabled":"Enabled","Disabled":"Désactivé","Back":"Retour","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"Un nouveau carnet \"%s\" va être créé et le fichier \"%s\" va être importé dedans","Please create a notebook first.":"Veuillez d'abord sélectionner un carnet.","Note title:":"Titre de la note :","Please create a notebook first":"Veuillez d'abord créer un carnet d'abord","To-do title:":"Titre de la tâche :","Notebook title:":"Titre du carnet :","Add or remove tags:":"Modifier les étiquettes :","Separate each tag by a comma.":"Séparez chaque étiquette par une virgule.","Rename notebook:":"Renommer le carnet :","Set alarm:":"Set alarm:","Layout":"Disposition","Some items cannot be synchronised.":"Some items cannot be synchronised.","View them now":"","Some items cannot be decrypted.":"Some items cannot be decrypted.","Set the password":"","Add or remove tags":"Gérer les étiquettes","Switch between note and to-do type":"Alterner entre note et tâche","Delete":"Supprimer","Delete notes?":"Supprimer les notes ?","No notes in here. Create one by clicking on \"New note\".":"Pas de notes ici. Créez-en une en pressant le bouton \"Nouvelle note\".","There is currently no notebook. Create one by clicking on \"New notebook\".":"There is currently no notebook. Create one by clicking on \"New notebook\".","Unsupported link or message: %s":"Lien ou message non géré : %s","Attach file":"Attacher un fichier","Set alarm":"Set alarm","Refresh":"Rafraîchir","Clear":"Supprimer","OneDrive Login":"Connexion OneDrive","Import":"Importer","Synchronisation Status":"Synchronisation Status","Encryption Options":"","Remove this tag from all the notes?":"Enlever cette étiquette de toutes les notes ?","Remove this search from the sidebar?":"Enlever cette recherche de la barre latérale ?","Rename":"Renommer","Synchronise":"Synchroniser","Notebooks":"Carnets","Tags":"Étiquettes","Searches":"Recherches","Please select where the sync status should be exported to":"Please select where the sync status should be exported to","Usage: %s":"Utilisation : %s","Unknown flag: %s":"Paramètre inconnu : %s","File system":"Système de fichier","OneDrive":"OneDrive","OneDrive Dev (For testing only)":"OneDrive Dév (Pour tester uniquement)","Unknown log level: %s":"Paramètre inconnu : %s","Unknown level ID: %s":"Paramètre inconnu : %s","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"Impossible de rafraîchir la connexion à OneDrive. Démarrez la synchronisation à nouveau pour corriger le problème.","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"","Cannot access %s":"Impossible d'accéder à %s","Created local items: %d.":"Objets créés localement : %d.","Updated local items: %d.":"Objets mis à jour localement : %d.","Created remote items: %d.":"Objets distants créés : %d.","Updated remote items: %d.":"Objets distants mis à jour : %d.","Deleted local items: %d.":"Objets supprimés localement : %d.","Deleted remote items: %d.":"Objets distants supprimés : %d.","State: \"%s\".":"État : \"%s\".","Cancelling...":"Annulation...","Completed: %s":"Terminé : %s","Synchronisation is already in progress. State: %s":"La synchronisation est déjà en cours. État : %s","Conflicts":"Conflits","A notebook with this title already exists: \"%s\"":"Un carnet avec ce titre existe déjà : \"%s\"","Notebooks cannot be named \"%s\", which is a reserved title.":"Les carnets ne peuvent être nommés \"%s\" car c'est un nom réservé.","Untitled":"Sans titre","This note does not have geolocation information.":"Cette note n'a pas d'information d'emplacement.","Cannot copy note to \"%s\" notebook":"Impossible de copier la note vers le carnet \"%s\"","Cannot move note to \"%s\" notebook":"Impossible de déplacer la note vers le carnet \"%s\"","Text editor":"Éditeur de texte","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"L'éditeur de texte pour ouvrir et modifier les notes. Si aucun n'est spécifié, il sera détecté automatiquement.","Language":"Langue","Date format":"Format de la date","Time format":"Format de l'heure","Theme":"Apparence","Light":"Clair","Dark":"Sombre","Show uncompleted todos on top of the lists":"Tâches non-terminées en haut des listes","Save geo-location with notes":"Enregistrer l'emplacement avec les notes","Synchronisation interval":"Intervalle de synchronisation","%d minutes":"%d minutes","%d hour":"%d heure","%d hours":"%d heures","Automatically update the application":"Mettre à jour le logiciel automatiquement","Show advanced options":"Montrer les options avancées","Synchronisation target":"Cible de la synchronisation","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"La cible avec laquelle synchroniser. Pour synchroniser avec le système de fichier, veuillez spécifier le répertoire avec `sync.2.path`.","Directory to synchronise with (absolute path)":"","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"Le chemin du répertoire avec lequel synchroniser lorsque la synchronisation par système de fichier est activée. Voir `sync.target`.","Invalid option value: \"%s\". Possible values are: %s.":"Option invalide: \"%s\". Les valeurs possibles sont : %s.","Items that cannot be synchronised":"","\"%s\": \"%s\"":"","Sync status (synced items / total items)":"Status de la synchronisation (objets synchro. / total)","%s: %d/%d":"%s: %d/%d","Total: %d/%d":"Total : %d/%d","Conflicted: %d":"Conflits : %d","To delete: %d":"A supprimer : %d","Folders":"Carnets","%s: %d notes":"%s : %d notes","Coming alarms":"Alarmes à venir","On %s: %s":"Le %s : %s","There are currently no notes. Create one by clicking on the (+) button.":"Ce carnet ne contient aucune note. Créez-en une en appuyant sur le bouton (+).","Delete these notes?":"Supprimer ces notes ?","Log":"Journal","Export Debug Report":"Exporter rapport de débogage","Configuration":"Configuration","Move to notebook...":"Déplacer la note vers carnet...","Move %d notes to notebook \"%s\"?":"Déplacer %d notes vers carnet \"%s\" ?","Select date":"Sélectionner date","Confirm":"Confirmer","Cancel synchronisation":"Annuler synchronisation","The notebook could not be saved: %s":"Ce carnet n'a pas pu être sauvegardé : %s","Edit notebook":"Éditer le carnet","This note has been modified:":"Cette note a été modifiée :","Save changes":"Enregistrer les changements","Discard changes":"Ignorer les changements","Unsupported image type: %s":"Type d'image non géré : %s","Attach photo":"Attacher une photo","Attach any file":"Attacher un fichier","Convert to note":"Convertir en note","Convert to todo":"Convertir en tâche","Hide metadata":"Cacher les métadonnées","Show metadata":"Afficher les métadonnées","View on map":"Voir emplacement sur carte","Delete notebook":"Supprimer le carnet","Login with OneDrive":"Se connecter à OneDrive","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"Appuyez sur le bouton (+) pour créer une nouvelle note ou carnet. Ouvrez le menu latéral pour accéder à vos carnets.","You currently have no notebook. Create one by clicking on (+) button.":"Vous n'avez pour l'instant pas de carnets. Créez-en un en pressant le bouton (+).","Welcome":"Bienvenue"} \ No newline at end of file diff --git a/ElectronClient/app/locales/hr_HR.json b/ElectronClient/app/locales/hr_HR.json new file mode 100644 index 000000000..16198201f --- /dev/null +++ b/ElectronClient/app/locales/hr_HR.json @@ -0,0 +1 @@ +{"Give focus to next pane":"Fokusiraj sljedeće okno","Give focus to previous pane":"Fokusiraj prethodno okno","Enter command line mode":"Otvori naredbeni redak","Exit command line mode":"Napusti naredbeni redak","Edit the selected note":"Uredi odabranu bilješku","Cancel the current command.":"Prekini trenutnu naredbu.","Exit the application.":"Izađi iz aplikacije.","Delete the currently selected note or notebook.":"Obriši odabranu bilješku ili bilježnicu.","To delete a tag, untag the associated notes.":"Da bi mogao obrisati oznaku, skini oznaku s povezanih bilješki.","Please select the note or notebook to be deleted first.":"Odaberi bilješku ili bilježnicu za brisanje.","Set a to-do as completed / not completed":"Postavi zadatak kao završen/nezavršen","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"[t]oggle [c]onsole between maximized/minimized/hidden/visible.","Search":"Traži","[t]oggle note [m]etadata.":"[t]oggle note [m]etadata.","[M]ake a new [n]ote":"[M]ake a new [n]ote","[M]ake a new [t]odo":"[M]ake a new [t]odo","[M]ake a new note[b]ook":"[M]ake a new note[b]ook","Copy ([Y]ank) the [n]ote to a notebook.":"Copy ([Y]ank) the [n]ote to a notebook.","Move the note to a notebook.":"Premjesti bilješku u bilježnicu.","Press Ctrl+D or type \"exit\" to exit the application":"Pritisni Ctrl+D ili napiši \"exit\" za izlazak iz aplikacije","More than one item match \"%s\". Please narrow down your query.":"Više od jednog rezultata odgovara \"%s\". Promijeni upit.","No notebook selected.":"Nije odabrana bilježnica.","No notebook has been specified.":"Nije specificirana bilježnica.","Y":"Y","n":"n","N":"N","y":"y","Cancelling background synchronisation... Please wait.":"Prekid sinkronizacije u pozadini... Pričekaj.","No such command: %s":"Ne postoji naredba: %s","The command \"%s\" is only available in GUI mode":"Naredba \"%s\" postoji samo u inačici s grafičkim sučeljem","Missing required argument: %s":"Nedostaje obavezni argument: %s","%s: %s":"%s: %s","Your choice: ":"Tvoj izbor: ","Invalid answer: %s":"Nevažeći odgovor: %s","Attaches the given file to the note.":"Prilaže datu datoteku bilješci.","Cannot find \"%s\".":"Ne mogu naći \"%s\".","Displays the given note.":"Prikazuje datu bilješku.","Displays the complete information about note.":"Prikazuje potpunu informaciju o bilješci.","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.","Also displays unset and hidden config variables.":"Također prikazuje nepostavljene i skrivene konfiguracijske varijable.","%s = %s (%s)":"%s = %s (%s)","%s = %s":"%s = %s","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.","Marks a to-do as done.":"Označava zadatak završenim.","Note is not a to-do: \"%s\"":"Bilješka nije zadatak: \"%s\"","Edit note.":"Uredi bilješku.","No text editor is defined. Please set it using `config editor `":"No text editor is defined. Please set it using `config editor `","No active notebook.":"Nema aktivne bilježnice.","Note does not exist: \"%s\". Create it?":"Bilješka ne postoji: \"%s\". Napravi je?","Starting to edit note. Close the editor to get back to the prompt.":"Starting to edit note. Close the editor to get back to the prompt.","Note has been saved.":"Bilješka je spremljena.","Exits the application.":"Izlaz iz aplikacije.","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"Izvozi podatke u dati direktorij. Po defaultu izvozi sve podatke uključujući bilježnice, bilješke, zadatke i resurse.","Exports only the given note.":"Izvozi samo datu bilješku.","Exports only the given notebook.":"Izvozi samo datu bilježnicu.","Displays a geolocation URL for the note.":"Prikazuje geolokacijski URL bilješke.","Displays usage information.":"Prikazuje informacije o korištenju.","Shortcuts are not available in CLI mode.":"Prečaci nisu podržani u naredbenom retku.","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"Upiši `help [naredba]` za više informacija o naredbi ili `help all` za sve informacije o korištenju.","The possible commands are:":"Moguće naredbe su:","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.","To move from one pane to another, press Tab or Shift+Tab.":"Za prijelaz iz jednog okna u drugo, pritisni Tab ili Shift+Tab.","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"Use the arrows and page up/down to scroll the lists and text areas (including this console).","To maximise/minimise the console, press \"TC\".":"Za maksimiziranje/minimiziranje konzole, pritisni \"TC\".","To enter command line mode, press \":\"":"Za ulaz u naredbeni redak, pritisni \":\"","To exit command line mode, press ESCAPE":"Za izlaz iz naredbenog retka, pritisni Esc","For the complete list of available keyboard shortcuts, type `help shortcuts`":"Za potpunu listu mogućih prečaca, upiši `help shortcuts`","Imports an Evernote notebook file (.enex file).":"Uvozi Evernote bilježnicu (.enex datoteku).","Do not ask for confirmation.":"Ne pitaj za potvrdu.","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"Datoteka \"%s\" će biti uvezena u postojeću bilježnicu \"%s\". Nastavi?","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"Nova bilježnica \"%s\" će biti stvorena i datoteka \"%s\" će biti uvezena u nju. Nastavi?","Found: %d.":"Nađeno: %d.","Created: %d.":"Stvoreno: %d.","Updated: %d.":"Ažurirano: %d.","Skipped: %d.":"Preskočeno: %d.","Resources: %d.":"Resursi: %d.","Tagged: %d.":"Označeno: %d.","Importing notes...":"Uvozim bilješke...","The notes have been imported: %s":"Bilješke su uvezene: %s","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"Prikazuje bilješke u trenutnoj bilježnici. Upiši `ls /` za prikaz liste bilježnica.","Displays only the first top notes.":"Prikaži samo prvih bilješki.","Sorts the item by (eg. title, updated_time, created_time).":"Sorts the item by (eg. title, updated_time, created_time).","Reverses the sorting order.":"Mijenja redoslijed.","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.","Either \"text\" or \"json\"":"Ili \"text\" ili \"json\"","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE","Please select a notebook first.":"Odaberi bilježnicu.","Creates a new notebook.":"Stvara novu bilježnicu.","Creates a new note.":"Stvara novu bilješku.","Notes can only be created within a notebook.":"Bilješke je moguće stvoriti samo u sklopu bilježnice.","Creates a new to-do.":"Stvara novi zadatak.","Moves the notes matching to [notebook].":"Premješta podudarajuće bilješke u [bilježnicu].","Renames the given (note or notebook) to .":"Renames the given (note or notebook) to .","Deletes the given notebook.":"Briše datu bilježnicu.","Deletes the notebook without asking for confirmation.":"Briše bilježnicu bez traženja potvrde.","Delete notebook? All notes within this notebook will also be deleted.":"Obrisati bilježnicu? Sve bilješke u toj bilježnici će također biti obrisane.","Deletes the notes matching .":"Deletes the notes matching .","Deletes the notes without asking for confirmation.":"Briše bilješke bez traženja potvrde.","%d notes match this pattern. Delete them?":"%d bilješki se podudara s pojmom pretraživanja. Obriši ih?","Delete note?":"Obrisati bilješku?","Searches for the given in all the notes.":"Searches for the given in all the notes.","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"Sets the property of the given to the given [value]. Possible properties are:\n\n%s","Displays summary about the notes and notebooks.":"Prikazuje sažetak o bilješkama i bilježnicama.","Synchronises with remote storage.":"Sinkronizira sa udaljenom pohranom podataka.","Sync to provided target (defaults to sync.target config value)":"Sinkroniziraj sa metom (default je polje sync.target u konfiguraciji)","Synchronisation is already in progress.":"Sinkronizacija je već u toku.","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"Ako sinkronizacija nije u toku, obriši lock datoteku u \"%s\" i nastavi...","Authentication was not completed (did not receive an authentication token).":"Authentication was not completed (did not receive an authentication token).","Synchronisation target: %s (%s)":"Meta sinkronizacije: %s (%s)","Cannot initialize synchroniser.":"Ne mogu započeti sinkronizaciju.","Starting synchronisation...":"Započinjem sinkronizaciju...","Cancelling... Please wait.":"Prekidam... Pričekaj."," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":" can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.","Invalid command: \"%s\"":"Nevažeća naredba: \"%s\""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":" can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.","Marks a to-do as non-completed.":"Označava zadatak kao nezavršen.","Switches to [notebook] - all further operations will happen within this notebook.":"Switches to [notebook] - all further operations will happen within this notebook.","Displays version information":"Prikazuje verziju","%s %s (%s)":"%s %s (%s)","Enum":"Enumeracija","Type: %s.":"Vrsta: %s.","Possible values: %s.":"Moguće vrijednosti: %s.","Default: %s":"Default: %s","Possible keys/values:":"Mogući ključevi/vrijednosti:","Fatal error:":"Fatalna greška:","The application has been authorised - you may now close this browser tab.":"Aplikacija je autorizirana - smiješ zatvoriti karticu preglednika.","The application has been successfully authorised.":"Aplikacija je uspješno autorizirana.","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"Otvori sljedeći URL u pregledniku da bi ovjerio aplikaciju. Aplikacija će stvoriti direktorij u \"Apps/Joplin\" i koristiti će samo taj direktorij za čitanje i pisanje. Aplikacija neće imati pristup osobnim podacima niti ičemu izvan tog direktorija. Nijedan se podatak neće dijeliti s trećom stranom.","Search:":"Traži:","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.","File":"Datoteka","New note":"Nova bilješka","New to-do":"Novi zadatak","New notebook":"Nova bilježnica","Import Evernote notes":"Uvezi Evernote bilješke","Evernote Export Files":"Evernote izvozne datoteke","Quit":"Izađi","Edit":"Uredi","Copy":"Kopiraj","Cut":"Izreži","Paste":"Zalijepi","Search in all the notes":"Pretraži u svim bilješkama","Tools":"Alati","Synchronisation status":"Status sinkronizacije","Options":"Opcije","Help":"Pomoć","Website and documentation":"Website i dokumentacija","About Joplin":"O Joplinu","%s %s (%s, %s)":"%s %s (%s, %s)","OK":"U redu","Cancel":"Odustani","Notes and settings are stored in: %s":"Bilješke i postavke su pohranjene u: %s","Save":"Spremi","Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?":"","Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.":"","Disable encryption":"","Enable encryption":"","Master Keys":"","Active":"","ID":"ID","Source":"Izvor","Created":"Stvoreno","Updated":"Ažurirano","Password":"","Password OK":"","Note: Only one master key is going to be used for encryption (the one marked as \"active\"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.":"","Status":"Status","Encryption is:":"","Enabled":"Enabled","Disabled":"Onemogućeno","Back":"Natrag","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"Nova bilježnica \"%s\" će biti stvorena i datoteka \"%s\" će biti uvezena u nju","Please create a notebook first.":"Prvo stvori bilježnicu.","Note title:":"Naslov bilješke:","Please create a notebook first":"Prvo stvori bilježnicu","To-do title:":"Naslov zadatka:","Notebook title:":"Naslov bilježnice:","Add or remove tags:":"Dodaj ili makni oznake:","Separate each tag by a comma.":"Odvoji oznake zarezom.","Rename notebook:":"Preimenuj bilježnicu:","Set alarm:":"Postavi upozorenje:","Layout":"Izgled","Some items cannot be synchronised.":"Neke stavke se ne mogu sinkronizirati.","View them now":"Pogledaj ih sada","Some items cannot be decrypted.":"Some items cannot be decrypted.","Set the password":"","Add or remove tags":"Dodaj ili makni oznake","Switch between note and to-do type":"Zamijeni bilješku i zadatak","Delete":"Obriši","Delete notes?":"Obriši bilješke?","No notes in here. Create one by clicking on \"New note\".":"Ovdje nema bilješki. Stvori novu pritiskom na \"Nova bilješka\".","There is currently no notebook. Create one by clicking on \"New notebook\".":"Ovdje nema bilježnica. Stvori novu pritiskom na \"Nova bilježnica\".","Unsupported link or message: %s":"Nepodržana poveznica ili poruka: %s","Attach file":"Priloži datoteku","Set alarm":"Postavi upozorenje","Refresh":"Osvježi","Clear":"Očisti","OneDrive Login":"OneDrive Login","Import":"Uvoz","Synchronisation Status":"Status Sinkronizacije","Encryption Options":"","Remove this tag from all the notes?":"Makni ovu oznaku iz svih bilješki?","Remove this search from the sidebar?":"Makni ovu pretragu iz izbornika?","Rename":"Preimenuj","Synchronise":"Sinkroniziraj","Notebooks":"Bilježnice","Tags":"Oznake","Searches":"Pretraživanja","Please select where the sync status should be exported to":"Odaberi lokaciju za izvoz statusa sinkronizacije","Usage: %s":"Korištenje: %s","Unknown flag: %s":"Nepoznata zastavica: %s","File system":"Datotečni sustav","OneDrive":"OneDrive","OneDrive Dev (For testing only)":"OneDrive Dev (Samo za testiranje)","Unknown log level: %s":"Nepoznata razina logiranja: %s","Unknown level ID: %s":"Nepoznat ID razine: %s","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"Nedostaju podaci za ovjeru. Pokušaj ponovo započeti sinkronizaciju.","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"Ne mogu sinkronizirati OneDrive.\n\nOva greška se često javlja pri korištenju usluge OneDrive for Business koja nije podržana.\n\nMolimo da koristite obični OneDrive korisnički račun.","Cannot access %s":"Ne mogu pristupiti %s","Created local items: %d.":"Stvorene lokalne stavke: %d.","Updated local items: %d.":"Ažurirane lokalne stavke: %d.","Created remote items: %d.":"Stvorene udaljene stavke: %d.","Updated remote items: %d.":"Ažurirane udaljene stavke: %d.","Deleted local items: %d.":"Obrisane lokalne stavke: %d.","Deleted remote items: %d.":"Obrisane udaljene stavke: %d.","State: \"%s\".":"Stanje: \"%s\".","Cancelling...":"Prekidam...","Completed: %s":"Dovršeno: %s","Synchronisation is already in progress. State: %s":"Sinkronizacija je već u toku. Stanje: %s","Conflicts":"Sukobi","A notebook with this title already exists: \"%s\"":"Bilježnica s ovim naslovom već postoji: \"%s\"","Notebooks cannot be named \"%s\", which is a reserved title.":"Naslov \"%s\" je rezerviran i ne može se koristiti.","Untitled":"Nenaslovljen","This note does not have geolocation information.":"Ova bilješka nema geolokacijske informacije.","Cannot copy note to \"%s\" notebook":"Ne mogu kopirati bilješku u bilježnicu %s","Cannot move note to \"%s\" notebook":"Ne mogu premjestiti bilješku u bilježnicu %s","Text editor":"Uređivač teksta","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"Program za uređivanje koji će biti korišten za uređivanje bilješki. Ako ni jedan nije odabran, pokušati će se sa default programom.","Language":"Jezik","Date format":"Format datuma","Time format":"Format vremena","Theme":"Tema","Light":"Svijetla","Dark":"Tamna","Show uncompleted todos on top of the lists":"Prikaži nezavršene zadatke na vrhu liste","Save geo-location with notes":"Spremi geolokacijske podatke sa bilješkama","Synchronisation interval":"Interval sinkronizacije","%d minutes":"%d minuta","%d hour":"%d sat","%d hours":"%d sati","Automatically update the application":"Automatsko instaliranje nove verzije","Show advanced options":"Prikaži napredne opcije","Synchronisation target":"Sinkroniziraj sa","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"Meta sinkronizacije. U slučaju sinkroniziranja s vlastitim datotečnim sustavom, postavi `sync.2.path` na ciljani direktorij.","Directory to synchronise with (absolute path)":"Direktorij za sinkroniziranje (apsolutna putanja)","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"Putanja do direktorija za sinkronizaciju u slučaju kad je sinkronizacija sa datotečnim sustavom omogućena. Vidi `sync.target`.","Invalid option value: \"%s\". Possible values are: %s.":"Nevažeća vrijednost: \"%s\". Moguće vrijednosti su: %s.","Items that cannot be synchronised":"Stavke koje se ne mogu sinkronizirati","\"%s\": \"%s\"":"\"%s\": \"%s\"","Sync status (synced items / total items)":"Status (sinkronizirane stavke / ukupni broj stavki)","%s: %d/%d":"%s: %d/%d","Total: %d/%d":"Ukupno: %d/%d","Conflicted: %d":"U sukobu: %d","To delete: %d":"Za brisanje: %d","Folders":"Mape","%s: %d notes":"%s: %d notes","Coming alarms":"Nadolazeća upozorenja","On %s: %s":"On %s: %s","There are currently no notes. Create one by clicking on the (+) button.":"Trenutno nema bilješki. Stvori novu klikom na (+) gumb.","Delete these notes?":"Obriši ove bilješke?","Log":"Log","Export Debug Report":"Izvezi Debug izvještaj","Configuration":"Konfiguracija","Move to notebook...":"Premjesti u bilježnicu...","Move %d notes to notebook \"%s\"?":"Premjesti %d bilješke u bilježnicu \"%s\"?","Select date":"Odaberi datum","Confirm":"Potvrdi","Cancel synchronisation":"Prekini sinkronizaciju","The notebook could not be saved: %s":"Bilježnicu nije moguće snimiti: %s","Edit notebook":"Uredi bilježnicu","This note has been modified:":"Bilješka je promijenjena:","Save changes":"Spremi promjene","Discard changes":"Odbaci promjene","Unsupported image type: %s":"Nepodržana vrsta slike: %s","Attach photo":"Priloži sliku","Attach any file":"Priloži datoteku","Convert to note":"Pretvori u bilješku","Convert to todo":"Pretvori u zadatak","Hide metadata":"Sakrij metapodatke","Show metadata":"Prikaži metapodatke","View on map":"Vidi na karti","Delete notebook":"Obriši bilježnicu","Login with OneDrive":"Prijavi se u OneDrive","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"Klikni (+) gumb za dodavanje nove bilješke ili bilježnice ili odaberi postojeću bilježnicu iz izbornika.","You currently have no notebook. Create one by clicking on (+) button.":"Trenutno nemaš nijednu bilježnicu. Stvori novu klikom na (+) gumb.","Welcome":"Dobro došli"} \ No newline at end of file diff --git a/ElectronClient/app/locales/index.js b/ElectronClient/app/locales/index.js index 86f261cfa..5c8581a2f 100644 --- a/ElectronClient/app/locales/index.js +++ b/ElectronClient/app/locales/index.js @@ -2,7 +2,12 @@ var locales = {}; locales['en_GB'] = require('./en_GB.json'); locales['de_DE'] = require('./de_DE.json'); locales['es_CR'] = require('./es_CR.json'); +locales['es_ES'] = require('./es_ES.json'); locales['fr_FR'] = require('./fr_FR.json'); +locales['hr_HR'] = require('./hr_HR.json'); locales['it_IT'] = require('./it_IT.json'); +locales['ja_JP'] = require('./ja_JP.json'); locales['pt_BR'] = require('./pt_BR.json'); +locales['ru_RU'] = require('./ru_RU.json'); +locales['zh_CN'] = require('./zh_CN.json'); module.exports = { locales: locales }; \ No newline at end of file diff --git a/ElectronClient/app/locales/it_IT.json b/ElectronClient/app/locales/it_IT.json index 258e5c856..382f5ace2 100644 --- a/ElectronClient/app/locales/it_IT.json +++ b/ElectronClient/app/locales/it_IT.json @@ -1 +1 @@ -{"Give focus to next pane":"Pannello successivo","Give focus to previous pane":"Pannello precedente","Enter command line mode":"Accedi alla modalità linea di comando","Exit command line mode":"Esci dalla modalità linea di comando","Edit the selected note":"Modifica la nota selezionata","Cancel the current command.":"Cancella il comando corrente.","Exit the application.":"Esci dall'applicazione.","Delete the currently selected note or notebook.":"Elimina la nota o il blocco note selezionato.","To delete a tag, untag the associated notes.":"Elimina un'etichetta, togli l'etichetta associata alle note.","Please select the note or notebook to be deleted first.":"Per favore seleziona la nota o il blocco note da eliminare.","Set a to-do as completed / not completed":"Imposta un'attività come completata / non completata","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"Scegli lo s[t]ato della [c]onsole: massimizzato/minimizzato/nascosto/visibile.","Search":"Cerca","[t]oggle note [m]etadata.":"mos[t]ra/nascondi i [m]etadata nelle note.","[M]ake a new [n]ote":"Crea ([M]ake) una nuova [n]ota","[M]ake a new [t]odo":"Crea ([M]ake) una nuova at[t]ività","[M]ake a new note[b]ook":"Crea ([M]ake) un nuovo [b]locco note","Copy ([Y]ank) the [n]ote to a notebook.":"Copia ([Y]) la [n]ota in un blocco note.","Move the note to a notebook.":"Sposta la nota in un blocco note.","Press Ctrl+D or type \"exit\" to exit the application":"Premi Ctrl+D o digita \"exit\" per uscire dall'applicazione","More than one item match \"%s\". Please narrow down your query.":"Più di un elemento corrisponde a \"%s\". Per favore restringi la ricerca.","No notebook selected.":"Nessun blocco note selezionato.","No notebook has been specified.":"Nessun blocco note è statoi specificato.","Y":"S","n":"n","N":"N","y":"s","Cancelling background synchronisation... Please wait.":"Cancellazione della sincronizzazione in background... Attendere prego.","No such command: %s":"Nessun comando: %s","The command \"%s\" is only available in GUI mode":"Il comando \"%s\" è disponibile solo nella modalità grafica","Missing required argument: %s":"Argomento richiesto mancante: %s","%s: %s":"%s: %s","Your choice: ":"La tua scelta: ","Invalid answer: %s":"Risposta non valida: %s","Attaches the given file to the note.":"Allega il seguente file alla nota.","Cannot find \"%s\".":"Non posso trovare \"%s\".","Displays the given note.":"Mostra la seguente nota.","Displays the complete information about note.":"Mostra le informazioni complete sulla nota.","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"Ricevi o imposta un valore di configurazione. se [value] non è impostato, verrà mostrato il valore del [name]. Se sia [name] che [valore] sono impostati, verrà mostrata la configurazione corrente.","Also displays unset and hidden config variables.":"Mostra anche le variabili di configurazione non impostate o nascoste.","%s = %s (%s)":"%s = %s (%s)","%s = %s":"%s = %s","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"Duplica le note che corrispondono a nel [notebook]. Se nessun blocco note è specificato, la nota viene duplicata nel blocco note corrente.","Marks a to-do as done.":"Segna un'attività come completata.","Note is not a to-do: \"%s\"":"La nota non è un'attività: \"%s\"","Edit note.":"Modifica nota.","No text editor is defined. Please set it using `config editor `":"Non è definito nessun editor di testo. Per favore impostalo usando `config editor `","No active notebook.":"Nessun blocco note attivo.","Note does not exist: \"%s\". Create it?":"Non esiste la nota: \"%s\". Desideri crearla?","Starting to edit note. Close the editor to get back to the prompt.":"Comincia a modificare la nota. Chiudi l'editor per tornare al prompt.","Note has been saved.":"La nota è stata salvata.","Exits the application.":"Esci dall'applicazione.","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"Esporta i dati da Joplin nella directory selezionata. Come impostazione predefinita verrà esportato il database completo, inclusi blocchi note, note, etichette e risorse.","Exports only the given note.":"Esporta solo la seguente nota.","Exports only the given notebook.":"Esporta solo il seguente blocco note.","Displays a geolocation URL for the note.":"Mostra l'URL di geolocalizzazione per la nota.","Displays usage information.":"Mostra le informazioni di utilizzo.","Shortcuts are not available in CLI mode.":"Le scorciatoie non sono disponibili nella modalità CLI.","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.","The possible commands are:":"I possibili comandi sono:","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"In ciascun comando, si deve necessariamente definire una nota o un blocco note usando un titolo, un ID o usando le scorciatoie `$n` or `$b` per , rispettivamente, la nota o il blocco note selezionato `$c` può essere usato per fare riferimento all'elemento selezionato.","To move from one pane to another, press Tab or Shift+Tab.":"Per passare da un pannello all'altro, premi Tab o Shift+Tab.","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"Usa le frecce e pagina su/giù per scorrere le liste e le aree di testo (compresa questa console).","To maximise/minimise the console, press \"TC\".":"Per massimizzare/minimizzare la console, premi \"TC\".","To enter command line mode, press \":\"":"Per entrare nella modalità command line, premi \":\"","To exit command line mode, press ESCAPE":"Per uscire dalla modalità command line, premi ESC","For the complete list of available keyboard shortcuts, type `help shortcuts`":"Per la lista completa delle scorciatoie disponibili, digita `help shortcuts`","Imports an Evernote notebook file (.enex file).":"Importa un file blocco note di Evernote (.enex file).","Do not ask for confirmation.":"Non chiedere conferma.","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"Il file \"%s\" sarà importato nel blocco note esistente \"%s\". Continuare?","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"Un nuovo blocco note \"%s\" sarà creato e al suo interno verrà importato il file \"%s\" . Continuare?","Found: %d.":"Trovato: %d.","Created: %d.":"Creato: %d.","Updated: %d.":"Aggiornato: %d.","Skipped: %d.":"Saltato: %d.","Resources: %d.":"Risorse: %d.","Tagged: %d.":"Etichettato: %d.","Importing notes...":"Importazione delle note...","The notes have been imported: %s":"Le note sono state importate: %s","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"Mostra le note nel seguente blocco note. Usa `ls /` per mostrare la lista dei blocchi note.","Displays only the first top notes.":"Mostra solo le prima note.","Sorts the item by (eg. title, updated_time, created_time).":"Ordina per (es. titolo, ultimo aggiornamento, creazione).","Reverses the sorting order.":"Inverti l'ordine.","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"Mostra solo gli elementi del tipo specificato. Possono essere `n` per le note, `t` per le attività o `nt` per note e attività. (es. `-tt` mostrerà solo le attività, mentre `-ttd` mostrerà sia note che attività.","Either \"text\" or \"json\"":"Sia \"testo\" che \"json\"","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"Usa un formato lungo di lista. Il formato è ID, NOTE_COUNT (per i blocchi note), DATE, TODO_CHECKED (per le attività), TITLE","Please select a notebook first.":"Per favore prima seleziona un blocco note.","Creates a new notebook.":"Crea un nuovo blocco note.","Creates a new note.":"Crea una nuova nota.","Notes can only be created within a notebook.":"Le note possono essere create all'interno de blocco note.","Creates a new to-do.":"Crea una nuova attività.","Moves the notes matching to [notebook].":"Sposta le note che corrispondono a in [notebook].","Renames the given (note or notebook) to .":"Rinomina (nota o blocco note) in .","Deletes the given notebook.":"Elimina il seguente blocco note.","Deletes the notebook without asking for confirmation.":"Elimina il blocco note senza richiedere una conferma.","Delete notebook? All notes within this notebook will also be deleted.":"","Deletes the notes matching .":"Elimina le note che corrispondono a .","Deletes the notes without asking for confirmation.":"Elimina le note senza chiedere conferma.","%d notes match this pattern. Delete them?":"%d note corrispondono. Eliminarle?","Delete note?":"Eliminare la nota?","Searches for the given in all the notes.":"Cerca in tutte le note.","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"Sets the property of the given to the given [value]. Possible properties are:\n\n%s","Displays summary about the notes and notebooks.":"Mostra un sommario delle note e dei blocchi note.","Synchronises with remote storage.":"Sincronizza con l'archivio remoto.","Sync to provided target (defaults to sync.target config value)":"Sincronizza con l'obiettivo fornito (come predefinito il valore di configurazione sync.target)","Synchronisation is already in progress.":"La sincronizzazione è in corso.","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"Trovato un file di blocco. Se si è certi che non è in corso alcuna sincronizzazione, è possibile eliminare il file di blocco in \"% s\" e riprendere l'operazione.","Authentication was not completed (did not receive an authentication token).":"Autenticazione non completata (non è stato ricevuto alcun token di autenticazione).","Synchronisation target: %s (%s)":"Posizione di sincronizzazione: %s (%s)","Cannot initialize synchroniser.":"Non è possibile inizializzare il sincronizzatore.","Starting synchronisation...":"Inizio sincronizzazione...","Cancelling... Please wait.":"Cancellazione... Attendere per favore."," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":" può essere \"add\", \"remove\" or \"list\" per assegnare o rimuovere [tag] da [note], o per mostrare le note associate a [tag]. Il comando `tag list` può essere usato per mostrare tutte le etichette.","Invalid command: \"%s\"":"Comando non valido: \"%s\""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":" può essere \"toggle\" or \"clear\". Usa \"toggle\" per cambiare lo stato dell'attività tra completata/non completata (se l'oggetto è una normale nota, questa verrà convertita in un'attività). Usa \"clear\" convertire le attività in normali note.","Marks a to-do as non-completed.":"Marca un'attività come non completata.","Switches to [notebook] - all further operations will happen within this notebook.":"Passa tra [notebook] - tutte le ulteriori operazioni interesseranno il seguente blocco note.","Displays version information":"Mostra le informazioni sulla versione","%s %s (%s)":"%s %s (%s)","Enum":"Enumerare","Type: %s.":"Tipo: %s.","Possible values: %s.":"Valori possibili: %s.","Default: %s":"Predefinito: %s","Possible keys/values:":"Chiave/valore possibili:","Fatal error:":"Errore fatale:","The application has been authorised - you may now close this browser tab.":"L'applicazione è stata autorizzata - puoi chiudere questo tab del tuo browser.","The application has been successfully authorised.":"L'applicazione è stata autorizzata con successo.","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"Per favore apri il seguente URL nel tuo browser per autenticare l'applicazione. L'applicazione creerà una directory in \"Apps/Joplin\" e leggerà/scriverà file solo in questa directory. Non avrà accesso a nessun file all'esterno di questa directory o ad alcun dato personale. Nessun dato verrà condiviso con terze parti.","Search:":"Cerca:","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"","File":"File","New note":"Nuova nota","New to-do":"Nuova attività","New notebook":"Nuovo blocco note","Import Evernote notes":"Importa le note da Evernote","Evernote Export Files":"Esposta i files di Evernote","Quit":"Esci","Edit":"Modifica","Copy":"Copia","Cut":"Taglia","Paste":"Incolla","Search in all the notes":"Cerca in tutte le note","Tools":"Strumenti","Synchronisation status":"Stato di sincronizzazione","Options":"Opzioni","Help":"Aiuto","Website and documentation":"Sito web e documentazione","About Joplin":"Informazione si Joplin","%s %s (%s, %s)":"%s %s (%s, %s)","OK":"OK","Cancel":"Cancella","Notes and settings are stored in: %s":"","Save":"","Back":"Indietro","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"Il nuovo blocco note \"%s\" verrà creato e \"%s\" vi verrà importato","Please create a notebook first.":"Per favore prima crea un blocco note.","Note title:":"Titolo della Nota:","Please create a notebook first":"Per favore prima crea un blocco note","To-do title:":"Titolo dell'attività:","Notebook title:":"Titolo del blocco note:","Add or remove tags:":"Aggiungi or rimuovi etichetta:","Separate each tag by a comma.":"Separa ogni etichetta da una virgola.","Rename notebook:":"Rinomina il blocco note:","Set alarm:":"Imposta allarme:","Layout":"Disposizione","Some items cannot be synchronised.":"Alcuni elementi non possono essere sincronizzati.","View them now":"Mostrali ora","ID":"","Source":"","Created":"Created","Updated":"Updated","Add or remove tags":"Aggiungi o rimuovi etichetta","Switch between note and to-do type":"Passa da un tipo di nota a un elenco di attività","Delete":"Elimina","Delete notes?":"Eliminare le note?","No notes in here. Create one by clicking on \"New note\".":"Non è presente nessuna nota. Creane una cliccando \"Nuova nota\".","There is currently no notebook. Create one by clicking on \"New notebook\".":"There is currently no notebook. Create one by clicking on \"New notebook\".","Unsupported link or message: %s":"Collegamento o messaggio non supportato: %s","Attach file":"Allega file","Set alarm":"Imposta allarme","Refresh":"Aggiorna","Clear":"Pulisci","OneDrive Login":"Login OneDrive","Import":"Importa","Synchronisation Status":"Stato della Sincronizzazione","Remove this tag from all the notes?":"Rimuovere questa etichetta da tutte le note?","Remove this search from the sidebar?":"Rimuovere questa ricerca dalla barra laterale?","Rename":"Rinomina","Synchronise":"Sincronizza","Notebooks":"Blocchi note","Tags":"Etichette","Searches":"Ricerche","Please select where the sync status should be exported to":"Please select where the sync status should be exported to","Usage: %s":"Uso: %s","Unknown flag: %s":"Etichetta sconosciuta: %s","File system":"File system","OneDrive":"OneDrive","OneDrive Dev (For testing only)":"OneDrive Dev (solo per test)","Unknown log level: %s":"Livello di log sconosciuto: %s","Unknown level ID: %s":"Livello ID sconosciuto: %s","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"Non è possibile aggiornare il token. mancano i dati di autenticazione. Ricominciare la sincronizzazione da capo potrebbe risolvere il problema.","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"Impossibile sincronizzare con OneDrive.\n\nQuesto errore spesso accade quando si utilizza OneDrive for Business, che purtroppo non può essere supportato.\n\nSi prega di considerare l'idea di utilizzare un account OneDrive normale.","Cannot access %s":"Non è possibile accedere a %s","Created local items: %d.":"Elementi locali creati: %d.","Updated local items: %d.":"Elementi locali aggiornati: %d.","Created remote items: %d.":"Elementi remoti creati: %d.","Updated remote items: %d.":"Elementi remoti aggiornati: %d.","Deleted local items: %d.":"Elementi locali eliminati: %d.","Deleted remote items: %d.":"Elementi remoti eliminati: %d.","State: \"%s\".":"Stato: \"%s\".","Cancelling...":"Cancellazione...","Completed: %s":"Completata: %s","Synchronisation is already in progress. State: %s":"La sincronizzazione è già in corso. Stato: %s","Conflicts":"Conflitti","A notebook with this title already exists: \"%s\"":"Esiste già un blocco note col titolo \"%s\"","Notebooks cannot be named \"%s\", which is a reserved title.":"I blocchi non possono essere chiamati \"%s\". È un titolo riservato.","Untitled":"Senza titolo","This note does not have geolocation information.":"Questa nota non ha informazione sulla geolocalizzazione.","Cannot copy note to \"%s\" notebook":"Non posso copiare la nota nel blocco note \"%s\"","Cannot move note to \"%s\" notebook":"Non posso spostare la nota nel blocco note \"%s\"","Text editor":"Editor di testo","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"L'editor che sarà usato per aprire la nota. Se nessun editor è specificato si cercherà di individuare automaticamente l'editor predefinito.","Language":"Linguaggio","Date format":"Formato della data","Time format":"Formato dell'orario","Theme":"Tema","Light":"Chiaro","Dark":"Scuro","Show uncompleted todos on top of the lists":"Mostra todo inclompleti in cima alla lista","Save geo-location with notes":"Salva geo-localizzazione con le note","Synchronisation interval":"Intervallo di sincronizzazione","Disabled":"Disabilitato","%d minutes":"%d minuti","%d hour":"%d ora","%d hours":"%d ore","Automatically update the application":"Aggiorna automaticamente l'applicazione","Show advanced options":"Mostra opzioni avanzate","Synchronisation target":"Destinazione di sincronizzazione","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"La destinazione della sincronizzazione. Se si sincronizza con il file system, impostare ' Sync. 2. Path ' per specificare la directory di destinazione.","Directory to synchronise with (absolute path)":"","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"Il percorso di sincronizzazione quando la sincronizzazione è abilitata. Vedi `sync.target`.","Invalid option value: \"%s\". Possible values are: %s.":"Oprione non valida: \"%s\". I valori possibili sono: %s.","Items that cannot be synchronised":"Elementi che non possono essere sincronizzati","\"%s\": \"%s\"":"\"%s\": \"%s\"","Sync status (synced items / total items)":"Stato di sincronizzazione (Elementi sincronizzati / Elementi totali)","%s: %d/%d":"%s: %d/%d","Total: %d/%d":"Totale: %d %d","Conflicted: %d":"In conflitto: %d","To delete: %d":"Da cancellare: %d","Folders":"Cartelle","%s: %d notes":"%s: %d note","Coming alarms":"Avviso imminente","On %s: %s":"Su %s: %s","There are currently no notes. Create one by clicking on the (+) button.":"Al momento non ci sono note. Creane una cliccando sul bottone (+).","Delete these notes?":"Cancellare queste note?","Log":"Log","Status":"Stato","Export Debug Report":"Esporta il Report di Debug","Configuration":"Configurazione","Move to notebook...":"Sposta sul blocco note...","Move %d notes to notebook \"%s\"?":"Spostare le note %d sul blocco note \"%s\"?","Select date":"Seleziona la data","Confirm":"Conferma","Cancel synchronisation":"Cancella la sincronizzazione","The notebook could not be saved: %s":"Il blocco note non può essere salvato: %s","Edit notebook":"Modifica blocco note","This note has been modified:":"Questa note è stata modificata:","Save changes":"Salva i cambiamenti","Discard changes":"Ignora modifiche","Unsupported image type: %s":"Tipo di immagine non supportata: %s","Attach photo":"Allega foto","Attach any file":"Allega qualsiasi file","Convert to note":"Converti in nota","Convert to todo":"Converti in Todo","Hide metadata":"Nascondi i Metadati","Show metadata":"Mostra i metadati","View on map":"Guarda sulla mappa","Delete notebook":"Cancella blocco note","Login with OneDrive":"Accedi a OneDrive","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"Fare clic sul pulsante (+) per creare una nuova nota o un nuovo blocco note. Fare clic sul menu laterale per accedere ai tuoi blocchi note esistenti.","You currently have no notebook. Create one by clicking on (+) button.":"Attualmente non hai nessun blocco note. Crearne uno cliccando sul pulsante (+).","Welcome":"Benvenuto"} \ No newline at end of file +{"Give focus to next pane":"Pannello successivo","Give focus to previous pane":"Pannello precedente","Enter command line mode":"Accedi alla modalità linea di comando","Exit command line mode":"Esci dalla modalità linea di comando","Edit the selected note":"Modifica la nota selezionata","Cancel the current command.":"Cancella il comando corrente.","Exit the application.":"Esci dall'applicazione.","Delete the currently selected note or notebook.":"Elimina la nota o il blocco note selezionato.","To delete a tag, untag the associated notes.":"Elimina un'etichetta, togli l'etichetta associata alle note.","Please select the note or notebook to be deleted first.":"Per favore seleziona la nota o il blocco note da eliminare.","Set a to-do as completed / not completed":"Imposta un'attività come completata / non completata","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"Scegli lo s[t]ato della [c]onsole: massimizzato/minimizzato/nascosto/visibile.","Search":"Cerca","[t]oggle note [m]etadata.":"mos[t]ra/nascondi i [m]etadata nelle note.","[M]ake a new [n]ote":"Crea ([M]ake) una nuova [n]ota","[M]ake a new [t]odo":"Crea ([M]ake) una nuova at[t]ività","[M]ake a new note[b]ook":"Crea ([M]ake) un nuovo [b]locco note","Copy ([Y]ank) the [n]ote to a notebook.":"Copia ([Y]) la [n]ota in un blocco note.","Move the note to a notebook.":"Sposta la nota in un blocco note.","Press Ctrl+D or type \"exit\" to exit the application":"Premi Ctrl+D o digita \"exit\" per uscire dall'applicazione","More than one item match \"%s\". Please narrow down your query.":"Più di un elemento corrisponde a \"%s\". Per favore restringi la ricerca.","No notebook selected.":"Nessun blocco note selezionato.","No notebook has been specified.":"Nessun blocco note è statoi specificato.","Y":"S","n":"n","N":"N","y":"s","Cancelling background synchronisation... Please wait.":"Cancellazione della sincronizzazione in background... Attendere prego.","No such command: %s":"Nessun comando: %s","The command \"%s\" is only available in GUI mode":"Il comando \"%s\" è disponibile solo nella modalità grafica","Missing required argument: %s":"Argomento richiesto mancante: %s","%s: %s":"%s: %s","Your choice: ":"La tua scelta: ","Invalid answer: %s":"Risposta non valida: %s","Attaches the given file to the note.":"Allega il seguente file alla nota.","Cannot find \"%s\".":"Non posso trovare \"%s\".","Displays the given note.":"Mostra la seguente nota.","Displays the complete information about note.":"Mostra le informazioni complete sulla nota.","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"Ricevi o imposta un valore di configurazione. se [value] non è impostato, verrà mostrato il valore del [name]. Se sia [name] che [valore] sono impostati, verrà mostrata la configurazione corrente.","Also displays unset and hidden config variables.":"Mostra anche le variabili di configurazione non impostate o nascoste.","%s = %s (%s)":"%s = %s (%s)","%s = %s":"%s = %s","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"Duplica le note che corrispondono a nel [notebook]. Se nessun blocco note è specificato, la nota viene duplicata nel blocco note corrente.","Marks a to-do as done.":"Segna un'attività come completata.","Note is not a to-do: \"%s\"":"La nota non è un'attività: \"%s\"","Edit note.":"Modifica nota.","No text editor is defined. Please set it using `config editor `":"Non è definito nessun editor di testo. Per favore impostalo usando `config editor `","No active notebook.":"Nessun blocco note attivo.","Note does not exist: \"%s\". Create it?":"Non esiste la nota: \"%s\". Desideri crearla?","Starting to edit note. Close the editor to get back to the prompt.":"Comincia a modificare la nota. Chiudi l'editor per tornare al prompt.","Note has been saved.":"La nota è stata salvata.","Exits the application.":"Esci dall'applicazione.","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"Esporta i dati da Joplin nella directory selezionata. Come impostazione predefinita verrà esportato il database completo, inclusi blocchi note, note, etichette e risorse.","Exports only the given note.":"Esporta solo la seguente nota.","Exports only the given notebook.":"Esporta solo il seguente blocco note.","Displays a geolocation URL for the note.":"Mostra l'URL di geolocalizzazione per la nota.","Displays usage information.":"Mostra le informazioni di utilizzo.","Shortcuts are not available in CLI mode.":"Le scorciatoie non sono disponibili nella modalità CLI.","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.","The possible commands are:":"I possibili comandi sono:","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"In ciascun comando, si deve necessariamente definire una nota o un blocco note usando un titolo, un ID o usando le scorciatoie `$n` or `$b` per , rispettivamente, la nota o il blocco note selezionato `$c` può essere usato per fare riferimento all'elemento selezionato.","To move from one pane to another, press Tab or Shift+Tab.":"Per passare da un pannello all'altro, premi Tab o Shift+Tab.","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"Usa le frecce e pagina su/giù per scorrere le liste e le aree di testo (compresa questa console).","To maximise/minimise the console, press \"TC\".":"Per massimizzare/minimizzare la console, premi \"TC\".","To enter command line mode, press \":\"":"Per entrare nella modalità command line, premi \":\"","To exit command line mode, press ESCAPE":"Per uscire dalla modalità command line, premi ESC","For the complete list of available keyboard shortcuts, type `help shortcuts`":"Per la lista completa delle scorciatoie disponibili, digita `help shortcuts`","Imports an Evernote notebook file (.enex file).":"Importa un file blocco note di Evernote (.enex file).","Do not ask for confirmation.":"Non chiedere conferma.","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"Il file \"%s\" sarà importato nel blocco note esistente \"%s\". Continuare?","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"Un nuovo blocco note \"%s\" sarà creato e al suo interno verrà importato il file \"%s\" . Continuare?","Found: %d.":"Trovato: %d.","Created: %d.":"Creato: %d.","Updated: %d.":"Aggiornato: %d.","Skipped: %d.":"Saltato: %d.","Resources: %d.":"Risorse: %d.","Tagged: %d.":"Etichettato: %d.","Importing notes...":"Importazione delle note...","The notes have been imported: %s":"Le note sono state importate: %s","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"Mostra le note nel seguente blocco note. Usa `ls /` per mostrare la lista dei blocchi note.","Displays only the first top notes.":"Mostra solo le prima note.","Sorts the item by (eg. title, updated_time, created_time).":"Ordina per (es. titolo, ultimo aggiornamento, creazione).","Reverses the sorting order.":"Inverti l'ordine.","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"Mostra solo gli elementi del tipo specificato. Possono essere `n` per le note, `t` per le attività o `nt` per note e attività. (es. `-tt` mostrerà solo le attività, mentre `-ttd` mostrerà sia note che attività.","Either \"text\" or \"json\"":"Sia \"testo\" che \"json\"","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"Usa un formato lungo di lista. Il formato è ID, NOTE_COUNT (per i blocchi note), DATE, TODO_CHECKED (per le attività), TITLE","Please select a notebook first.":"Per favore prima seleziona un blocco note.","Creates a new notebook.":"Crea un nuovo blocco note.","Creates a new note.":"Crea una nuova nota.","Notes can only be created within a notebook.":"Le note possono essere create all'interno de blocco note.","Creates a new to-do.":"Crea una nuova attività.","Moves the notes matching to [notebook].":"Sposta le note che corrispondono a in [notebook].","Renames the given (note or notebook) to .":"Rinomina (nota o blocco note) in .","Deletes the given notebook.":"Elimina il seguente blocco note.","Deletes the notebook without asking for confirmation.":"Elimina il blocco note senza richiedere una conferma.","Delete notebook? All notes within this notebook will also be deleted.":"","Deletes the notes matching .":"Elimina le note che corrispondono a .","Deletes the notes without asking for confirmation.":"Elimina le note senza chiedere conferma.","%d notes match this pattern. Delete them?":"%d note corrispondono. Eliminarle?","Delete note?":"Eliminare la nota?","Searches for the given in all the notes.":"Cerca in tutte le note.","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"Sets the property of the given to the given [value]. Possible properties are:\n\n%s","Displays summary about the notes and notebooks.":"Mostra un sommario delle note e dei blocchi note.","Synchronises with remote storage.":"Sincronizza con l'archivio remoto.","Sync to provided target (defaults to sync.target config value)":"Sincronizza con l'obiettivo fornito (come predefinito il valore di configurazione sync.target)","Synchronisation is already in progress.":"La sincronizzazione è in corso.","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"Trovato un file di blocco. Se si è certi che non è in corso alcuna sincronizzazione, è possibile eliminare il file di blocco in \"% s\" e riprendere l'operazione.","Authentication was not completed (did not receive an authentication token).":"Autenticazione non completata (non è stato ricevuto alcun token di autenticazione).","Synchronisation target: %s (%s)":"Posizione di sincronizzazione: %s (%s)","Cannot initialize synchroniser.":"Non è possibile inizializzare il sincronizzatore.","Starting synchronisation...":"Inizio sincronizzazione...","Cancelling... Please wait.":"Cancellazione... Attendere per favore."," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":" può essere \"add\", \"remove\" or \"list\" per assegnare o rimuovere [tag] da [note], o per mostrare le note associate a [tag]. Il comando `tag list` può essere usato per mostrare tutte le etichette.","Invalid command: \"%s\"":"Comando non valido: \"%s\""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":" può essere \"toggle\" or \"clear\". Usa \"toggle\" per cambiare lo stato dell'attività tra completata/non completata (se l'oggetto è una normale nota, questa verrà convertita in un'attività). Usa \"clear\" convertire le attività in normali note.","Marks a to-do as non-completed.":"Marca un'attività come non completata.","Switches to [notebook] - all further operations will happen within this notebook.":"Passa tra [notebook] - tutte le ulteriori operazioni interesseranno il seguente blocco note.","Displays version information":"Mostra le informazioni sulla versione","%s %s (%s)":"%s %s (%s)","Enum":"Enumerare","Type: %s.":"Tipo: %s.","Possible values: %s.":"Valori possibili: %s.","Default: %s":"Predefinito: %s","Possible keys/values:":"Chiave/valore possibili:","Fatal error:":"Errore fatale:","The application has been authorised - you may now close this browser tab.":"L'applicazione è stata autorizzata - puoi chiudere questo tab del tuo browser.","The application has been successfully authorised.":"L'applicazione è stata autorizzata con successo.","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"Per favore apri il seguente URL nel tuo browser per autenticare l'applicazione. L'applicazione creerà una directory in \"Apps/Joplin\" e leggerà/scriverà file solo in questa directory. Non avrà accesso a nessun file all'esterno di questa directory o ad alcun dato personale. Nessun dato verrà condiviso con terze parti.","Search:":"Cerca:","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"","File":"File","New note":"Nuova nota","New to-do":"Nuova attività","New notebook":"Nuovo blocco note","Import Evernote notes":"Importa le note da Evernote","Evernote Export Files":"Esposta i files di Evernote","Quit":"Esci","Edit":"Modifica","Copy":"Copia","Cut":"Taglia","Paste":"Incolla","Search in all the notes":"Cerca in tutte le note","Tools":"Strumenti","Synchronisation status":"Stato di sincronizzazione","Options":"Opzioni","Help":"Aiuto","Website and documentation":"Sito web e documentazione","About Joplin":"Informazione si Joplin","%s %s (%s, %s)":"%s %s (%s, %s)","OK":"OK","Cancel":"Cancella","Notes and settings are stored in: %s":"","Save":"","Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?":"","Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.":"","Disable encryption":"","Enable encryption":"","Master Keys":"","Active":"","ID":"","Source":"","Created":"Created","Updated":"Updated","Password":"","Password OK":"","Note: Only one master key is going to be used for encryption (the one marked as \"active\"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.":"","Status":"Stato","Encryption is:":"","Enabled":"Enabled","Disabled":"Disabilitato","Back":"Indietro","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"Il nuovo blocco note \"%s\" verrà creato e \"%s\" vi verrà importato","Please create a notebook first.":"Per favore prima crea un blocco note.","Note title:":"Titolo della Nota:","Please create a notebook first":"Per favore prima crea un blocco note","To-do title:":"Titolo dell'attività:","Notebook title:":"Titolo del blocco note:","Add or remove tags:":"Aggiungi or rimuovi etichetta:","Separate each tag by a comma.":"Separa ogni etichetta da una virgola.","Rename notebook:":"Rinomina il blocco note:","Set alarm:":"Imposta allarme:","Layout":"Disposizione","Some items cannot be synchronised.":"Alcuni elementi non possono essere sincronizzati.","View them now":"Mostrali ora","Some items cannot be decrypted.":"Some items cannot be decrypted.","Set the password":"","Add or remove tags":"Aggiungi o rimuovi etichetta","Switch between note and to-do type":"Passa da un tipo di nota a un elenco di attività","Delete":"Elimina","Delete notes?":"Eliminare le note?","No notes in here. Create one by clicking on \"New note\".":"Non è presente nessuna nota. Creane una cliccando \"Nuova nota\".","There is currently no notebook. Create one by clicking on \"New notebook\".":"There is currently no notebook. Create one by clicking on \"New notebook\".","Unsupported link or message: %s":"Collegamento o messaggio non supportato: %s","Attach file":"Allega file","Set alarm":"Imposta allarme","Refresh":"Aggiorna","Clear":"Pulisci","OneDrive Login":"Login OneDrive","Import":"Importa","Synchronisation Status":"Stato della Sincronizzazione","Encryption Options":"","Remove this tag from all the notes?":"Rimuovere questa etichetta da tutte le note?","Remove this search from the sidebar?":"Rimuovere questa ricerca dalla barra laterale?","Rename":"Rinomina","Synchronise":"Sincronizza","Notebooks":"Blocchi note","Tags":"Etichette","Searches":"Ricerche","Please select where the sync status should be exported to":"Please select where the sync status should be exported to","Usage: %s":"Uso: %s","Unknown flag: %s":"Etichetta sconosciuta: %s","File system":"File system","OneDrive":"OneDrive","OneDrive Dev (For testing only)":"OneDrive Dev (solo per test)","Unknown log level: %s":"Livello di log sconosciuto: %s","Unknown level ID: %s":"Livello ID sconosciuto: %s","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"Non è possibile aggiornare il token. mancano i dati di autenticazione. Ricominciare la sincronizzazione da capo potrebbe risolvere il problema.","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"Impossibile sincronizzare con OneDrive.\n\nQuesto errore spesso accade quando si utilizza OneDrive for Business, che purtroppo non può essere supportato.\n\nSi prega di considerare l'idea di utilizzare un account OneDrive normale.","Cannot access %s":"Non è possibile accedere a %s","Created local items: %d.":"Elementi locali creati: %d.","Updated local items: %d.":"Elementi locali aggiornati: %d.","Created remote items: %d.":"Elementi remoti creati: %d.","Updated remote items: %d.":"Elementi remoti aggiornati: %d.","Deleted local items: %d.":"Elementi locali eliminati: %d.","Deleted remote items: %d.":"Elementi remoti eliminati: %d.","State: \"%s\".":"Stato: \"%s\".","Cancelling...":"Cancellazione...","Completed: %s":"Completata: %s","Synchronisation is already in progress. State: %s":"La sincronizzazione è già in corso. Stato: %s","Conflicts":"Conflitti","A notebook with this title already exists: \"%s\"":"Esiste già un blocco note col titolo \"%s\"","Notebooks cannot be named \"%s\", which is a reserved title.":"I blocchi non possono essere chiamati \"%s\". È un titolo riservato.","Untitled":"Senza titolo","This note does not have geolocation information.":"Questa nota non ha informazione sulla geolocalizzazione.","Cannot copy note to \"%s\" notebook":"Non posso copiare la nota nel blocco note \"%s\"","Cannot move note to \"%s\" notebook":"Non posso spostare la nota nel blocco note \"%s\"","Text editor":"Editor di testo","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"L'editor che sarà usato per aprire la nota. Se nessun editor è specificato si cercherà di individuare automaticamente l'editor predefinito.","Language":"Linguaggio","Date format":"Formato della data","Time format":"Formato dell'orario","Theme":"Tema","Light":"Chiaro","Dark":"Scuro","Show uncompleted todos on top of the lists":"Mostra todo inclompleti in cima alla lista","Save geo-location with notes":"Salva geo-localizzazione con le note","Synchronisation interval":"Intervallo di sincronizzazione","%d minutes":"%d minuti","%d hour":"%d ora","%d hours":"%d ore","Automatically update the application":"Aggiorna automaticamente l'applicazione","Show advanced options":"Mostra opzioni avanzate","Synchronisation target":"Destinazione di sincronizzazione","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"La destinazione della sincronizzazione. Se si sincronizza con il file system, impostare ' Sync. 2. Path ' per specificare la directory di destinazione.","Directory to synchronise with (absolute path)":"","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"Il percorso di sincronizzazione quando la sincronizzazione è abilitata. Vedi `sync.target`.","Invalid option value: \"%s\". Possible values are: %s.":"Oprione non valida: \"%s\". I valori possibili sono: %s.","Items that cannot be synchronised":"Elementi che non possono essere sincronizzati","\"%s\": \"%s\"":"\"%s\": \"%s\"","Sync status (synced items / total items)":"Stato di sincronizzazione (Elementi sincronizzati / Elementi totali)","%s: %d/%d":"%s: %d/%d","Total: %d/%d":"Totale: %d %d","Conflicted: %d":"In conflitto: %d","To delete: %d":"Da cancellare: %d","Folders":"Cartelle","%s: %d notes":"%s: %d note","Coming alarms":"Avviso imminente","On %s: %s":"Su %s: %s","There are currently no notes. Create one by clicking on the (+) button.":"Al momento non ci sono note. Creane una cliccando sul bottone (+).","Delete these notes?":"Cancellare queste note?","Log":"Log","Export Debug Report":"Esporta il Report di Debug","Configuration":"Configurazione","Move to notebook...":"Sposta sul blocco note...","Move %d notes to notebook \"%s\"?":"Spostare le note %d sul blocco note \"%s\"?","Select date":"Seleziona la data","Confirm":"Conferma","Cancel synchronisation":"Cancella la sincronizzazione","The notebook could not be saved: %s":"Il blocco note non può essere salvato: %s","Edit notebook":"Modifica blocco note","This note has been modified:":"Questa note è stata modificata:","Save changes":"Salva i cambiamenti","Discard changes":"Ignora modifiche","Unsupported image type: %s":"Tipo di immagine non supportata: %s","Attach photo":"Allega foto","Attach any file":"Allega qualsiasi file","Convert to note":"Converti in nota","Convert to todo":"Converti in Todo","Hide metadata":"Nascondi i Metadati","Show metadata":"Mostra i metadati","View on map":"Guarda sulla mappa","Delete notebook":"Cancella blocco note","Login with OneDrive":"Accedi a OneDrive","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"Fare clic sul pulsante (+) per creare una nuova nota o un nuovo blocco note. Fare clic sul menu laterale per accedere ai tuoi blocchi note esistenti.","You currently have no notebook. Create one by clicking on (+) button.":"Attualmente non hai nessun blocco note. Crearne uno cliccando sul pulsante (+).","Welcome":"Benvenuto"} \ No newline at end of file diff --git a/ElectronClient/app/locales/ja_JP.json b/ElectronClient/app/locales/ja_JP.json new file mode 100644 index 000000000..d282bc2bd --- /dev/null +++ b/ElectronClient/app/locales/ja_JP.json @@ -0,0 +1 @@ +{"Give focus to next pane":"次のペインへ","Give focus to previous pane":"前のペインへ","Enter command line mode":"コマンドラインモードに入る","Exit command line mode":"コマンドラインモードの終了","Edit the selected note":"選択したノートを編集","Cancel the current command.":"現在のコマンドをキャンセル","Exit the application.":"アプリケーションを終了する","Delete the currently selected note or notebook.":"選択中のノートまたはノートブックを削除","To delete a tag, untag the associated notes.":"タグを削除するには、関連するノートからタグを外してください。","Please select the note or notebook to be deleted first.":"ます削除するノートかノートブックを選択してください。","Set a to-do as completed / not completed":"ToDoを完了/未完に設定","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"コンソールを最大表示/最小表示/非表示/可視で切り替える([t][c])","Search":"検索","[t]oggle note [m]etadata.":"ノートのメタ情報を切り替える [tm]","[M]ake a new [n]ote":"新しいノートの作成 [mn]","[M]ake a new [t]odo":"新しいToDoの作成 [mt]","[M]ake a new note[b]ook":"新しいノートブックの作成 [mb]","Copy ([Y]ank) the [n]ote to a notebook.":"ノートをノートブックにコピー [yn]","Move the note to a notebook.":"ノートをノートブックに移動","Press Ctrl+D or type \"exit\" to exit the application":"アプリケーションを終了するには、Ctrl+Dまたは\"exit\"と入力してください","More than one item match \"%s\". Please narrow down your query.":"一つ以上のアイテムが\"%s\"に一致しました。クエリを絞るようにしてください。","No notebook selected.":"ノートブックが選択されていません。","No notebook has been specified.":"ノートブックが選択されていません。","Y":"","n":"","N":"","y":"","Cancelling background synchronisation... Please wait.":"バックグラウンド同期を中止中… しばらくお待ちください。","No such command: %s":"コマンドが違います:%s","The command \"%s\" is only available in GUI mode":"コマンド \"%s\"は、GUIのみで有効です。","Missing required argument: %s":"引数が足りません:%s","%s: %s":"","Your choice: ":"選択:","Invalid answer: %s":"無効な入力:%s","Attaches the given file to the note.":"選択されたファイルをノートに添付","Cannot find \"%s\".":"\"%s\"は見つかりませんでした。","Displays the given note.":"選択されたノートを表示","Displays the complete information about note.":"ノートに関するすべての情報を表示","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"設定を行います。[value]がない場合は、[name]で示された設定項目の値を表示します。両方とも指定されていない場合は、現在の設定のリストを表示します。","Also displays unset and hidden config variables.":"未設定または非表示の設定項目も表示します。","%s = %s (%s)":"","%s = %s":"","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"に一致するノートを[notebook]に複製します。[notebook]が指定されていない場合は、現在のノートブックに複製を行います。","Marks a to-do as done.":"ToDoを完了として","Note is not a to-do: \"%s\"":"ノートはToDoリストではありません:\"%s\"","Edit note.":"ノートを編集する。","No text editor is defined. Please set it using `config editor `":"テキストエディタが設定されていません。`config editor `で設定を行ってください。","No active notebook.":"有効なbノートブックがありません。","Note does not exist: \"%s\". Create it?":"\"%s\"というノートはありません。お作りいたしますか?","Starting to edit note. Close the editor to get back to the prompt.":"ノートの編集の開始。エディタを閉じると元の画面に戻ることが出来ます。","Note has been saved.":"ノートは保存されました。","Exits the application.":"アプリケーションの終了。","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"Joplinのデータを選択されたディレクトリに出力する。標準では、ノートブック・ノート・タグ・添付データを含むすべてのデータベースを出力します。","Exports only the given note.":"選択されたノートのみを出力する。","Exports only the given notebook.":"選択されたノートブックのみを出力する。","Displays a geolocation URL for the note.":"ノートの位置情報URLを表示する。","Displays usage information.":"使い方を表示する。","Shortcuts are not available in CLI mode.":"CLIモードではショートカットは使用できません。","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"コマンドのさらなる情報は、`help [command]`で見ることが出来ます;または、`help all`ですべての使用方法の情報を表示できます。","The possible commands are:":"有効なコマンドは:","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"すべてのコマンドで、ノートまたはノートブックは、題名またはID、または選択中の物はそれぞれショートカット`$n`または`$b`で指定できます。`$c`で選択中のアイテムを参照できます。","To move from one pane to another, press Tab or Shift+Tab.":"ペイン間を移動するには、TabかShift+Tabをおしてください。","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"リストや入力エリアの移動には矢印キーまたはPage Up/Downを使用します。","To maximise/minimise the console, press \"TC\".":"コンソールの最大化・最小化には\"TC\"と入力してください。","To enter command line mode, press \":\"":"コマンドラインモードに入るには、\":\"を入力してください。","To exit command line mode, press ESCAPE":"コマンドラインモードを終了するには、ESCキーを押してください。","For the complete list of available keyboard shortcuts, type `help shortcuts`":"有効なすべてのキーボードショートカットを表示するには、`help shortcuts`と入力してください。","Imports an Evernote notebook file (.enex file).":"Evernoteノートブックファイル(.enex)のインポート","Do not ask for confirmation.":"確認を行わない。","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"ファイル \"%s\" はノートブック \"%s\"に取り込まれます。よろしいですか?","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"新しいノートブック\"%s\"が作成され、ファイル\"%s\"が取り込まれます。よろしいですか?","Found: %d.":"見つかりました:%d","Created: %d.":"作成しました:%d","Updated: %d.":"アップデートしました:%d","Skipped: %d.":"スキップしました:%d","Resources: %d.":"リソース:%d","Tagged: %d.":"タグ付き:%d","Importing notes...":"ノートのインポート…","The notes have been imported: %s":"ノートはインポートされました:%s","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"現在のノートブック中のノートを表示します。ノートブックのリストを表示するには、`ls /`と入力してください。","Displays only the first top notes.":"上位 件のノートを表示する。","Sorts the item by (eg. title, updated_time, created_time).":"アイテムをで並び替え (例: title, updated_time, created_time).","Reverses the sorting order.":"逆順に並び替える。","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.","Either \"text\" or \"json\"":"\"text\"または\"json\"のどちらか","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"長い形式のリストフォーマットを使用します。フォーマットは:ID, NOTE_COUNT (ノートブックのみ), DATE, TODO_CHECKED (ToDoのみ), TITLE","Please select a notebook first.":"ますはノートブックを選択して下さい。","Creates a new notebook.":"あたらしいノートブックを作成します。","Creates a new note.":"あたらしいノートを作成します。","Notes can only be created within a notebook.":"ノートは、ノートブック内のみで作ることが出来ます。","Creates a new to-do.":"新しいToDoを作成します。","Moves the notes matching to [notebook].":"に一致するアイテムを、[notebook]に移動します。","Renames the given (note or notebook) to .":" (ノートまたはノートブック)の名前を、に変更します。","Deletes the given notebook.":"指定されたノートブックを削除します。","Deletes the notebook without asking for confirmation.":"ノートブックを確認なしで削除します。","Delete notebook? All notes within this notebook will also be deleted.":"ノートブックを削除しますか?中にあるノートはすべて消えてしまいます。","Deletes the notes matching .":"に一致するノートを削除する。","Deletes the notes without asking for confirmation.":"ノートを確認なしで削除します。","%d notes match this pattern. Delete them?":"%d個のノートが一致しました。削除しますか?","Delete note?":"ノートを削除しますか?","Searches for the given in all the notes.":"指定されたをすべてのノートから検索する。","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"のプロパティ を、指示された[value]に設定します。有効なプロパティは:\n\n%s","Displays summary about the notes and notebooks.":"ノートとノートブックのサマリを表示します。","Synchronises with remote storage.":"リモート保存領域と同期します。","Sync to provided target (defaults to sync.target config value)":"指定のターゲットと同期します。(標準: sync.targetの設定値)","Synchronisation is already in progress.":"同期はすでに実行中です。","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"ロックファイルがすでに保持されています。同期作業が行われていない場合は、\"%s\"にあるロックファイルを削除して、作業を再度行ってください。","Authentication was not completed (did not receive an authentication token).":"認証は完了していません(認証トークンが得られませんでした)","Synchronisation target: %s (%s)":"同期先: %s (%s)","Cannot initialize synchroniser.":"同期プロセスを初期化できませんでした。","Starting synchronisation...":"同期を開始中...","Cancelling... Please wait.":"中止中...お待ちください。"," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":" は\"add\", \"remove\", \"list\"のいずれかで、指定したノートからタグをつけたり外したり出来ます。`tag list`で、すべてのタグを見ることが出来ます。","Invalid command: \"%s\"":"無効な命令: \"%s\""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":"は、\"toggle\"または\"clear\"を指定できます。\"toggle\"を指定すると、指定したToDoの完了済み/未完を反転できます。指定したノートが通常のノートであれば、ToDoに変換されます。\"clear\"を指定すると、ToDoを通常のノートに変換できます。","Marks a to-do as non-completed.":"ToDoを未完としてマーク","Switches to [notebook] - all further operations will happen within this notebook.":"ノートブック [notebook]に切り替え - これ以降の作業は、指定のノートブック内で行われます。","Displays version information":"バージョン情報の表示","%s %s (%s)":"","Enum":"列挙","Type: %s.":"種類: %s.","Possible values: %s.":"取り得る値: %s.","Default: %s":"規定値: %s","Possible keys/values:":"取り得るキーバリュー: ","Fatal error:":"致命的なエラー: ","The application has been authorised - you may now close this browser tab.":"アプリケーションは認証されました - ブラウザを閉じて頂いてかまいません。","The application has been successfully authorised.":"アプリケーションは問題なく認証されました。","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"このアプリケーションを認証するためには下記のURLをブラウザで開いてください。アプリケーションは\"Apps/Joplin\"フォルダを作成し、その中のファイルのみを読み書きします。あなたの個人的なファイルや、ディレクトリ外のファイルにはアクセスしません。第三者にデータが共有されることもありません。","Search:":"検索: ","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"Joplinへようこそ!\n\n`:help shortcuts`と入力することで、キーボードショートカットのリストを見ることが出来ます。また、`:help`で使い方を確認できます。\n\n例えば、ノートブックの作成には`mb`で出来、ノートの作成は`mn`で行うことが出来ます。","File":"ファイル","New note":"新しいノート","New to-do":"新しいToDo","New notebook":"新しいノートブック","Import Evernote notes":"Evernoteのインポート","Evernote Export Files":"Evernote Exportファイル","Quit":"終了","Edit":"編集","Copy":"コピー","Cut":"切り取り","Paste":"貼り付け","Search in all the notes":"すべてのノートを検索","Tools":"ツール","Synchronisation status":"同期状況","Options":"オプション","Help":"ヘルプ","Website and documentation":"Webサイトとドキュメント","About Joplin":"Joplinについて","%s %s (%s, %s)":"","OK":"","Cancel":"キャンセル","Notes and settings are stored in: %s":"ノートと設定は、%sに保存されます。","Save":"保存","Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?":"","Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.":"","Disable encryption":"","Enable encryption":"","Master Keys":"","Active":"アクティブ","ID":"","Source":"","Created":"Created","Updated":"Updated","Password":"","Password OK":"","Note: Only one master key is going to be used for encryption (the one marked as \"active\"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.":"注意:\"active\"に指定されたマスターキーのみが暗号化に使用されます。暗号化に使用されたキーの応じて、すべてのキーが暗号解除のために使用されます。","Status":"状態","Encryption is:":"","Enabled":"Enabled","Disabled":"無効","Back":"戻る","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"\"%s\"という名前の新しいノートブックが作成され、ファイル\"%s\"がインポートされます。","Please create a notebook first.":"ますはノートブックを作成して下さい。","Note title:":"ノートの題名:","Please create a notebook first":"ますはノートブックを作成して下さい。","To-do title:":"ToDoの題名:","Notebook title:":"ノートブックの題名:","Add or remove tags:":"タグの追加・削除:","Separate each tag by a comma.":"それぞれのタグをカンマ(,)で区切ってください。","Rename notebook:":"ノートブックの名前を変更:","Set alarm:":"アラームをセット:","Layout":"レイアウト","Some items cannot be synchronised.":"いくつかの項目は同期されませんでした。","View them now":"今すぐ表示","Some items cannot be decrypted.":"Some items cannot be decrypted.","Set the password":"","Add or remove tags":"タグの追加・削除","Switch between note and to-do type":"ノートとToDoを切り替え","Delete":"削除","Delete notes?":"ノートを削除しますか?","No notes in here. Create one by clicking on \"New note\".":"ノートがありません。新しいノートを作成して下さい。","There is currently no notebook. Create one by clicking on \"New notebook\".":"ノートブックがありません。新しいノートブックを作成してください。","Unsupported link or message: %s":"","Attach file":"ファイルを添付","Set alarm":"アラームをセット","Refresh":"更新","Clear":"クリア","OneDrive Login":"OneDriveログイン","Import":"インポート","Synchronisation Status":"同期状況","Encryption Options":"","Remove this tag from all the notes?":"すべてのノートからこのタグを削除しますか?","Remove this search from the sidebar?":"サイドバーからこの検索を削除しますか?","Rename":"名前の変更","Synchronise":"同期","Notebooks":"ノートブック","Tags":"タグ","Searches":"検索","Please select where the sync status should be exported to":"同期状況の出力先を選択してください","Usage: %s":"使用方法: %s","Unknown flag: %s":"不明なフラグ: %s","File system":"ファイルシステム","OneDrive":"","OneDrive Dev (For testing only)":"","Unknown log level: %s":"","Unknown level ID: %s":"","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"トークンの更新が出来ませんでした。認証データがありません。同期を再度行うことで解決することがあります。","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"OneDriveと同期できませんでした。\n\nOneDrive for Business(未サポート)を使用中はこのエラーが起こることがあります。\n\n通常のOneDriveアカウントの使用をご検討ください。","Cannot access %s":"%sにアクセスできません","Created local items: %d.":"ローカルアイテムの作成: %d.","Updated local items: %d.":"ローカルアイテムの更新: %d.","Created remote items: %d.":"リモートアイテムの作成: %d.","Updated remote items: %d.":"リモートアイテムの更新: %d.","Deleted local items: %d.":"ローカルアイテムの削除: %d.","Deleted remote items: %d.":"リモートアイテムの削除: %d.","State: \"%s\".":"状態: \"%s\"。","Cancelling...":"中止中...","Completed: %s":"完了: %s","Synchronisation is already in progress. State: %s":"同期作業はすでに実行中です。状態: %s","Conflicts":"衝突","A notebook with this title already exists: \"%s\"":"\"%s\"という名前のノートブックはすでに存在しています。","Notebooks cannot be named \"%s\", which is a reserved title.":"\"%s\"と言う名前はシステムで使用するために予約済みです。名前の変更が出来ません。","Untitled":"名称未設定","This note does not have geolocation information.":"このノートには位置情報がありません。","Cannot copy note to \"%s\" notebook":"ノートをノートブック \"%s\"にコピーできませんでした。","Cannot move note to \"%s\" notebook":"ノートをノートブック \"%s\"に移動できませんでした。","Text editor":"テキストエディタ","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"ノートを開くために使用されるエディタです。特に指定がなければ、デフォルトのエディタの検出を試みます。","Language":"言語","Date format":"日付の形式","Time format":"時刻の形式","Theme":"テーマ","Light":"明るい","Dark":"暗い","Show uncompleted todos on top of the lists":"未完のToDoをリストの上部に表示","Save geo-location with notes":"ノートに位置情報を保存","Synchronisation interval":"同期間隔","%d minutes":"%d 分","%d hour":"%d 時間","%d hours":"%d 時間","Automatically update the application":"アプリケーションの自動更新","Show advanced options":"詳細な設定の表示","Synchronisation target":"同期先","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"同期先です。ローカルのファイルシステムと同期する場合は、`sync.2.path`を同期先のディレクトリに設定してください。","Directory to synchronise with (absolute path)":"同期先のディレクトリ(絶対パス)","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"ファイルシステム同期の有効時に同期を行うパスです。`sync.target`も参考にしてください。","Invalid option value: \"%s\". Possible values are: %s.":"無効な設定値: \"%s\"。有効な値は: %sです。","Items that cannot be synchronised":"同期が出来なかったアイテム","\"%s\": \"%s\"":"","Sync status (synced items / total items)":"同期状況 (同期済/総数)","%s: %d/%d":"","Total: %d/%d":"総数: %d/%d","Conflicted: %d":"衝突: %d","To delete: %d":"削除予定: %d","Folders":"フォルダ","%s: %d notes":"%s: %d ノート","Coming alarms":"時間のきたアラーム","On %s: %s":"","There are currently no notes. Create one by clicking on the (+) button.":"ノートがありません。(+)ボタンを押して新しいノートを作成してください。","Delete these notes?":"ノートを削除しますか?","Log":"ログ","Export Debug Report":"デバッグレポートの出力","Configuration":"設定","Move to notebook...":"ノートブックへ移動...","Move %d notes to notebook \"%s\"?":"%d個のノートを\"%s\"に移動しますか?","Select date":"日付の選択","Confirm":"確認","Cancel synchronisation":"同期の中止","The notebook could not be saved: %s":"ノートブックは保存できませんでした:%s","Edit notebook":"ノートブックの編集","This note has been modified:":"ノートは変更されています:","Save changes":"変更を保存","Discard changes":"変更を破棄","Unsupported image type: %s":"サポートされていないイメージ形式: %s.","Attach photo":"写真を添付","Attach any file":"ファイルを添付","Convert to note":"ノートに変換","Convert to todo":"ToDoに変換","Hide metadata":"メタデータを隠す","Show metadata":"メタデータを表示","View on map":"地図上に表示","Delete notebook":"ノートブックを削除","Login with OneDrive":"OneDriveログイン","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"(+)ボタンを押してノートやノートブックを作成してください。サイドメニューからあなたのノートブックにアクセスが出来ます。","You currently have no notebook. Create one by clicking on (+) button.":"ノートブックがありません。(+)をクリックして新しいノートブックを作成してください。","Welcome":"ようこそ"} \ No newline at end of file diff --git a/ElectronClient/app/locales/pt_BR.json b/ElectronClient/app/locales/pt_BR.json index 19bf940c9..7518adf50 100644 --- a/ElectronClient/app/locales/pt_BR.json +++ b/ElectronClient/app/locales/pt_BR.json @@ -1 +1 @@ -{"Give focus to next pane":"Dar o foco para o próximo painel","Give focus to previous pane":"Dar o foco para o painel anterior","Enter command line mode":"Entrar no modo de linha de comando","Exit command line mode":"Sair do modo de linha de comando","Edit the selected note":"Editar nota selecionada","Cancel the current command.":"Cancelar comando atual.","Exit the application.":"Sair da aplicação.","Delete the currently selected note or notebook.":"Excluir nota selecionada ou notebook.","To delete a tag, untag the associated notes.":"Para eliminar uma tag, remova a tag das notas associadas a ela.","Please select the note or notebook to be deleted first.":"Por favor, primeiro, selecione a nota ou caderno a excluir.","Set a to-do as completed / not completed":"Marcar uma tarefa como completada / não completada","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"al[t]ernar [c]onsole entre maximizado / minimizado / oculto / visível.","Search":"Procurar","[t]oggle note [m]etadata.":"al[t]ernar [m]etadados de notas.","[M]ake a new [n]ote":"Criar ([M]ake) uma nova [n]ota","[M]ake a new [t]odo":"Criar ([M]ake) nova [t]arefa","[M]ake a new note[b]ook":"Criar ([M]ake) novo caderno (note[b]ook)","Copy ([Y]ank) the [n]ote to a notebook.":"Copiar ([Y]ank) a [n]ota a um caderno.","Move the note to a notebook.":"Mover nota para um caderno.","Press Ctrl+D or type \"exit\" to exit the application":"Digite Ctrl+D ou \"exit\" para sair da aplicação","More than one item match \"%s\". Please narrow down your query.":"Mais que um item combinam com \"%s\". Por favor, refine sua pesquisa.","No notebook selected.":"Nenhum caderno selecionado.","No notebook has been specified.":"Nenhum caderno foi especificado.","Y":"S","n":"n","N":"N","y":"s","Cancelling background synchronisation... Please wait.":"Cancelando sincronização em segundo plano... Por favor, aguarde.","No such command: %s":"No such command: %s","The command \"%s\" is only available in GUI mode":"O comando \"%s\" está disponível somente em modo gráfico","Missing required argument: %s":"Argumento requerido faltando: %s","%s: %s":"%s: %s","Your choice: ":"Sua escolha: ","Invalid answer: %s":"Resposta inválida: %s","Attaches the given file to the note.":"Anexa o arquivo dado à nota.","Cannot find \"%s\".":"Não posso encontrar \"%s\".","Displays the given note.":"Exibe a nota informada.","Displays the complete information about note.":"Exibe a informação completa sobre a nota.","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"Obtém ou define um valor de configuração. Se [valor] não for fornecido, ele mostrará o valor de [nome]. Se nem [nome] nem [valor] forem fornecidos, ele listará a configuração atual.","Also displays unset and hidden config variables.":"Também exibe variáveis de configuração não definidas e ocultas.","%s = %s (%s)":"%s = %s (%s)","%s = %s":"%s = %s","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"Duplica as notas que correspondem a para o [caderno]. Se nenhum caderno for especificado, a nota será duplicada no caderno atual.","Marks a to-do as done.":"Marca uma tarefa como feita.","Note is not a to-do: \"%s\"":"Nota não é uma tarefa: \"%s\"","Edit note.":"Editar nota.","No text editor is defined. Please set it using `config editor `":"Nenhum editor de texto está definido. Defina-o usando o comando `config edit `","No active notebook.":"Nenhum caderno ativo.","Note does not exist: \"%s\". Create it?":"A nota não existe: \"%s\". Criar?","Starting to edit note. Close the editor to get back to the prompt.":"Começando a editar a nota. Feche o editor para voltar ao prompt.","Note has been saved.":"Nota gravada.","Exits the application.":"Sai da aplicação.","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"Exporta os dados do Joplin para o diretório informado. Por padrão, ele exportará o banco de dados completo, incluindo cadernos, notas, tags e recursos.","Exports only the given note.":"Exporta apenas a nota fornecida.","Exports only the given notebook.":"Exporta apenas o caderno fornecido.","Displays a geolocation URL for the note.":"Exibe uma URL de geolocalização para a nota.","Displays usage information.":"Exibe informações de uso.","Shortcuts are not available in CLI mode.":"Os atalhos não estão disponíveis no modo CLI.","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.","The possible commands are:":"Os comandos possíveis são:","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"Em qualquer comando, uma nota ou caderno pode ser referenciado por título ou ID, ou usando os atalhos `$n` ou` $b` para, respectivamente, a nota ou caderno selecionado. `$c` pode ser usado para se referenciar ao item atualmente selecionado.","To move from one pane to another, press Tab or Shift+Tab.":"Para mover de um painel para outro, pressione Tab ou Shift + Tab.","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"Use as setas e a Page Up/Page Down para rolar as listas e áreas de texto (incluindo este console).","To maximise/minimise the console, press \"TC\".":"Para maximizar / minimizar o console, pressione \"TC\".","To enter command line mode, press \":\"":"Para entrar no modo de linha de comando, pressione \":\"","To exit command line mode, press ESCAPE":"Para sair do modo de linha de comando, pressione o ESC","For the complete list of available keyboard shortcuts, type `help shortcuts`":"Para a lista completa de atalhos de teclado disponíveis, digite `help shortcuts`","Imports an Evernote notebook file (.enex file).":"Importa um arquivo de caderno do Evernote (arquivo .enex).","Do not ask for confirmation.":"Não pedir confirmação.","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"O arquivo \"%s\" será importado para o caderno existente \"%s\". Continuar?","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"O novo caderno \"%s\" será criado e o arquivo \"%s\" será importado para ele. Continuar?","Found: %d.":"Encontrado: %d.","Created: %d.":"Criado: %d.","Updated: %d.":"Atualizado: %d.","Skipped: %d.":"Ignorado: %d.","Resources: %d.":"Recursos: %d.","Tagged: %d.":"Tag adicionada: %d.","Importing notes...":"Importando notas ...","The notes have been imported: %s":"As notas foram importadas: %s","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"Exibe as notas no caderno atual. Use `ls /` para exibir a lista de cadernos.","Displays only the first top notes.":"Exibe apenas as primeiras notas.","Sorts the item by (eg. title, updated_time, created_time).":"Classifica o item por (ex.: title, update_time, created_time).","Reverses the sorting order.":"Inverte a ordem de classificação.","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"Exibe apenas os itens do(s) tipo(s) específico(s). Pode ser `n` para notas,` t` para tarefas, ou `nt` para notas e tarefas (por exemplo.` -tt` exibiria apenas os itens pendentes, enquanto `-ttd` exibiria notas e tarefas .","Either \"text\" or \"json\"":"Ou \"text\" ou \"json\"","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"Use o formato da lista longa. O formato é ID, NOTE_COUNT (para caderno), DATE, TODO_CHECKED (para tarefas), TITLE","Please select a notebook first.":"Por favor, selecione um caderno primeiro.","Creates a new notebook.":"Cria um novo caderno.","Creates a new note.":"Cria uma nova nota.","Notes can only be created within a notebook.":"As notas só podem ser criadas dentro de um caderno.","Creates a new to-do.":"Cria uma nova tarefa.","Moves the notes matching to [notebook].":"Move as notas correspondentes para [caderno].","Renames the given (note or notebook) to .":"Renomeia o dado (nota ou caderno) para .","Deletes the given notebook.":"Exclui o caderno informado.","Deletes the notebook without asking for confirmation.":"Exclui o caderno sem pedir confirmação.","Delete notebook? All notes within this notebook will also be deleted.":"","Deletes the notes matching .":"Exclui as notas correspondentes ao padrão .","Deletes the notes without asking for confirmation.":"Exclui as notas sem pedir confirmação.","%d notes match this pattern. Delete them?":"%d notas correspondem a este padrão. Apagar todos?","Delete note?":"Apagar nota?","Searches for the given in all the notes.":"Procura o padrão em todas as notas.","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"Sets the property of the given to the given [value]. Possible properties are:\n\n%s","Displays summary about the notes and notebooks.":"Exibe sumário sobre as notas e cadernos.","Synchronises with remote storage.":"Sincroniza com o armazenamento remoto.","Sync to provided target (defaults to sync.target config value)":"Sincronizar para destino fornecido (p padrão é o valor de configuração sync.target)","Synchronisation is already in progress.":"A sincronização já está em andamento.","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"O arquivo de bloqueio já está ativo. Se você sabe que nenhuma sincronização está ocorrendo, você pode excluir o arquivo de bloqueio em \"%s\" e retomar a operação.","Authentication was not completed (did not receive an authentication token).":"A autenticação não foi concluída (não recebeu um token de autenticação).","Synchronisation target: %s (%s)":"Alvo de sincronização: %s (%s)","Cannot initialize synchroniser.":"Não é possível inicializar o sincronizador.","Starting synchronisation...":"Iniciando sincronização...","Cancelling... Please wait.":"Cancelando... Aguarde."," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":" pode ser \"add\", \"remove\" ou \"list\" para atribuir ou remover [tag] de [nota], ou para listar as notas associadas a [tag]. O comando `taglist` pode ser usado para listar todas as tags.","Invalid command: \"%s\"":"Comando inválido: \"%s\""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":" pode ser \"toggle\" ou \"clear\". Use \"toggle\" para alternar entre as tarefas entre o estado completo e incompleto (se o alvo for uma nota comum, ele será convertido em uma tarefa a fazer). Use \"clear\" para converter a tarefa em uma nota normal.","Marks a to-do as non-completed.":"Marca uma tarefa como não completada.","Switches to [notebook] - all further operations will happen within this notebook.":"Alterna para [caderno] - todas as operações adicionais acontecerão dentro deste caderno.","Displays version information":"Exibe informações da versão","%s %s (%s)":"%s %s (%s)","Enum":"Enum","Type: %s.":"Tipo: %s.","Possible values: %s.":"Valores possíveis: %s.","Default: %s":"Padrão: %s","Possible keys/values:":"Possíveis chaves/valores:","Fatal error:":"Erro fatal:","The application has been authorised - you may now close this browser tab.":"O aplicativo foi autorizado - agora você pode fechar esta guia do navegador.","The application has been successfully authorised.":"O aplicativo foi autorizado com sucesso.","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"Abra a seguinte URL no seu navegador para autenticar o aplicativo. O aplicativo criará um diretório em \"Apps/Joplin\" e somente lerá e gravará arquivos neste diretório. Não terá acesso a nenhum arquivo fora deste diretório nem a nenhum outro dado pessoal. Nenhum dado será compartilhado com terceiros.","Search:":"Procurar:","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"","File":"Arquivo","New note":"Nova nota","New to-do":"Nova tarefa","New notebook":"Novo caderno","Import Evernote notes":"Importar notas do Evernote","Evernote Export Files":"Arquivos de Exportação do Evernote","Quit":"Sair","Edit":"Editar","Copy":"Copiar","Cut":"Cortar","Paste":"Colar","Search in all the notes":"Pesquisar em todas as notas","Tools":"Ferramentas","Synchronisation status":"Synchronisation status","Options":"Opções","Help":"Ajuda","Website and documentation":"Website e documentação","About Joplin":"Sobre o Joplin","%s %s (%s, %s)":"%s %s (%s, %s)","OK":"OK","Cancel":"Cancelar","Notes and settings are stored in: %s":"","Save":"","Back":"Voltar","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"O novo caderno \"%s\" será criado e o arquivo \"%s\" será importado para ele","Please create a notebook first.":"Primeiro, crie um caderno.","Note title:":"Título da nota:","Please create a notebook first":"Primeiro, crie um caderno","To-do title:":"Título da tarefa:","Notebook title:":"Título do caderno:","Add or remove tags:":"Adicionar ou remover tags:","Separate each tag by a comma.":"Separe cada tag por vírgula.","Rename notebook:":"Renomear caderno:","Set alarm:":"Definir alarme:","Layout":"Layout","Some items cannot be synchronised.":"Some items cannot be synchronised.","View them now":"","ID":"","Source":"","Created":"Created","Updated":"Updated","Add or remove tags":"Adicionar ou remover tags","Switch between note and to-do type":"Alternar entre os tipos Nota e Tarefa","Delete":"Excluir","Delete notes?":"Excluir notas?","No notes in here. Create one by clicking on \"New note\".":"Não há notas aqui. Crie uma, clicando em \"Nova nota\".","There is currently no notebook. Create one by clicking on \"New notebook\".":"There is currently no notebook. Create one by clicking on \"New notebook\".","Unsupported link or message: %s":"Link ou mensagem não suportada: %s","Attach file":"Anexar arquivo","Set alarm":"Definir alarme","Refresh":"Atualizar","Clear":"Limpar (clear)","OneDrive Login":"Login no OneDrive","Import":"Importar","Synchronisation Status":"Synchronisation Status","Remove this tag from all the notes?":"Remover esta tag de todas as notas?","Remove this search from the sidebar?":"Remover essa pesquisa da barra lateral?","Rename":"Renomear","Synchronise":"Sincronizar","Notebooks":"Cadernos","Tags":"Tags","Searches":"Pesquisas","Please select where the sync status should be exported to":"Please select where the sync status should be exported to","Usage: %s":"Uso: %s","Unknown flag: %s":"Flag desconhecido: %s","File system":"Sistema de arquivos","OneDrive":"OneDrive","OneDrive Dev (For testing only)":"OneDrive Dev (apenas para testes)","Unknown log level: %s":"Nível de log desconhecido: %s","Unknown level ID: %s":"Nível ID desconhecido: %s","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"Não é possível atualizar token: faltam dados de autenticação. Iniciar a sincronização novamente pode corrigir o problema.","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"Não foi possível sincronizar com o OneDrive.\n\nEste erro geralmente acontece ao usar o OneDrive for Business, que infelizmente não pode ser suportado.\n\nConsidere usar uma conta regular do OneDrive.","Cannot access %s":"Não é possível acessar %s","Created local items: %d.":"Itens locais criados: %d.","Updated local items: %d.":"Itens locais atualizados: %d.","Created remote items: %d.":"Itens remotos criados: %d.","Updated remote items: %d.":"Itens remotos atualizados: %d.","Deleted local items: %d.":"Itens locais excluídos: %d.","Deleted remote items: %d.":"Itens remotos excluídos: %d.","State: \"%s\".":"Estado: \"%s\".","Cancelling...":"Cancelando...","Completed: %s":"Completado: %s","Synchronisation is already in progress. State: %s":"Sincronização já em andamento. Estado: %s","Conflicts":"Conflitos","A notebook with this title already exists: \"%s\"":"Já existe caderno com este título: \"%s\"","Notebooks cannot be named \"%s\", which is a reserved title.":"Os cadernos não podem ser nomeados como\"%s\", que é um título reservado.","Untitled":"Sem título","This note does not have geolocation information.":"Esta nota não possui informações de geolocalização.","Cannot copy note to \"%s\" notebook":"Não é possível copiar a nota para o caderno \"%s\" ","Cannot move note to \"%s\" notebook":"Não é possível mover a nota para o caderno \"%s\"","Text editor":"Editor de texto","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"O editor que será usado para abrir uma nota. Se nenhum for indicado, ele tentará detectar automaticamente o editor padrão.","Language":"Idioma","Date format":"Formato de data","Time format":"Formato de hora","Theme":"Tema","Light":"Light","Dark":"Dark","Show uncompleted todos on top of the lists":"Mostrar tarefas incompletas no topo das listas","Save geo-location with notes":"Salvar geolocalização com notas","Synchronisation interval":"Intervalo de sincronização","Disabled":"Desabilitado","%d minutes":"%d minutos","%d hour":"%d hora","%d hours":"%d horas","Automatically update the application":"Atualizar automaticamente o aplicativo","Show advanced options":"Mostrar opções avançadas","Synchronisation target":"Alvo de sincronização","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"O alvo para sincronizar. Se estiver sincronizando com o sistema de arquivos, configure `sync.2.path` para especificar o diretório de destino.","Directory to synchronise with (absolute path)":"","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"O caminho para sincronizar, quando a sincronização do sistema de arquivos está habilitada. Veja `sync.target`.","Invalid option value: \"%s\". Possible values are: %s.":"Valor da opção inválida: \"%s\". Os valores possíveis são: %s.","Items that cannot be synchronised":"","\"%s\": \"%s\"":"","Sync status (synced items / total items)":"Status de sincronização (sincronizados / totais)","%s: %d/%d":"%s: %d/%d","Total: %d/%d":"Total: %d/%d","Conflicted: %d":"Em conflito: %d","To delete: %d":"Para excluir: %d","Folders":"Pastas","%s: %d notes":"%s: %d notas","Coming alarms":"Próximos alarmes","On %s: %s":"Em %s: %s","There are currently no notes. Create one by clicking on the (+) button.":"Atualmente, não há notas. Crie uma, clicando no botão (+).","Delete these notes?":"Excluir estas notas?","Log":"Log","Status":"Status","Export Debug Report":"Exportar Relatório de Debug","Configuration":"Configuração","Move to notebook...":"Mover para o caderno...","Move %d notes to notebook \"%s\"?":"Mover %d notas para o caderno \"%s\"?","Select date":"Selecionar data","Confirm":"Confirmar","Cancel synchronisation":"Cancelar sincronização","The notebook could not be saved: %s":"O caderno não pôde ser salvo: %s","Edit notebook":"Editar caderno","This note has been modified:":"Esta nota foi modificada:","Save changes":"Gravar alterações","Discard changes":"Descartar alterações","Unsupported image type: %s":"Tipo de imagem não suportada: %s","Attach photo":"Anexar foto","Attach any file":"Anexar qualquer arquivo","Convert to note":"Converter para nota","Convert to todo":"Converter para tarefa","Hide metadata":"Ocultar metadados","Show metadata":"Exibir metadados","View on map":"Ver no mapa","Delete notebook":"Excluir caderno","Login with OneDrive":"Login com OneDrive","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"Clique no botão (+) para criar uma nova nota ou caderno. Clique no menu lateral para acessar seus cadernos existentes.","You currently have no notebook. Create one by clicking on (+) button.":"Você não possui cadernos. Crie um clicando no botão (+).","Welcome":"Bem-vindo"} \ No newline at end of file +{"Give focus to next pane":"Dar o foco para o próximo painel","Give focus to previous pane":"Dar o foco para o painel anterior","Enter command line mode":"Entrar no modo de linha de comando","Exit command line mode":"Sair do modo de linha de comando","Edit the selected note":"Editar nota selecionada","Cancel the current command.":"Cancelar comando atual.","Exit the application.":"Sair da aplicação.","Delete the currently selected note or notebook.":"Excluir nota selecionada ou notebook.","To delete a tag, untag the associated notes.":"Para eliminar uma tag, remova a tag das notas associadas a ela.","Please select the note or notebook to be deleted first.":"Por favor, primeiro, selecione a nota ou caderno a excluir.","Set a to-do as completed / not completed":"Marcar uma tarefa como completada / não completada","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"al[t]ernar [c]onsole entre maximizado / minimizado / oculto / visível.","Search":"Procurar","[t]oggle note [m]etadata.":"al[t]ernar [m]etadados de notas.","[M]ake a new [n]ote":"Criar ([M]ake) uma nova [n]ota","[M]ake a new [t]odo":"Criar ([M]ake) nova [t]arefa","[M]ake a new note[b]ook":"Criar ([M]ake) novo caderno (note[b]ook)","Copy ([Y]ank) the [n]ote to a notebook.":"Copiar ([Y]ank) a [n]ota a um caderno.","Move the note to a notebook.":"Mover nota para um caderno.","Press Ctrl+D or type \"exit\" to exit the application":"Digite Ctrl+D ou \"exit\" para sair da aplicação","More than one item match \"%s\". Please narrow down your query.":"Mais que um item combinam com \"%s\". Por favor, refine sua pesquisa.","No notebook selected.":"Nenhum caderno selecionado.","No notebook has been specified.":"Nenhum caderno foi especificado.","Y":"S","n":"n","N":"N","y":"s","Cancelling background synchronisation... Please wait.":"Cancelando sincronização em segundo plano... Por favor, aguarde.","No such command: %s":"No such command: %s","The command \"%s\" is only available in GUI mode":"O comando \"%s\" está disponível somente em modo gráfico","Missing required argument: %s":"Argumento requerido faltando: %s","%s: %s":"%s: %s","Your choice: ":"Sua escolha: ","Invalid answer: %s":"Resposta inválida: %s","Attaches the given file to the note.":"Anexa o arquivo dado à nota.","Cannot find \"%s\".":"Não posso encontrar \"%s\".","Displays the given note.":"Exibe a nota informada.","Displays the complete information about note.":"Exibe a informação completa sobre a nota.","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"Obtém ou define um valor de configuração. Se [valor] não for fornecido, ele mostrará o valor de [nome]. Se nem [nome] nem [valor] forem fornecidos, ele listará a configuração atual.","Also displays unset and hidden config variables.":"Também exibe variáveis de configuração não definidas e ocultas.","%s = %s (%s)":"%s = %s (%s)","%s = %s":"%s = %s","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"Duplica as notas que correspondem a para o [caderno]. Se nenhum caderno for especificado, a nota será duplicada no caderno atual.","Marks a to-do as done.":"Marca uma tarefa como feita.","Note is not a to-do: \"%s\"":"Nota não é uma tarefa: \"%s\"","Edit note.":"Editar nota.","No text editor is defined. Please set it using `config editor `":"Nenhum editor de texto está definido. Defina-o usando o comando `config edit `","No active notebook.":"Nenhum caderno ativo.","Note does not exist: \"%s\". Create it?":"A nota não existe: \"%s\". Criar?","Starting to edit note. Close the editor to get back to the prompt.":"Começando a editar a nota. Feche o editor para voltar ao prompt.","Note has been saved.":"Nota gravada.","Exits the application.":"Sai da aplicação.","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"Exporta os dados do Joplin para o diretório informado. Por padrão, ele exportará o banco de dados completo, incluindo cadernos, notas, tags e recursos.","Exports only the given note.":"Exporta apenas a nota fornecida.","Exports only the given notebook.":"Exporta apenas o caderno fornecido.","Displays a geolocation URL for the note.":"Exibe uma URL de geolocalização para a nota.","Displays usage information.":"Exibe informações de uso.","Shortcuts are not available in CLI mode.":"Os atalhos não estão disponíveis no modo CLI.","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.","The possible commands are:":"Os comandos possíveis são:","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"Em qualquer comando, uma nota ou caderno pode ser referenciado por título ou ID, ou usando os atalhos `$n` ou` $b` para, respectivamente, a nota ou caderno selecionado. `$c` pode ser usado para se referenciar ao item atualmente selecionado.","To move from one pane to another, press Tab or Shift+Tab.":"Para mover de um painel para outro, pressione Tab ou Shift + Tab.","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"Use as setas e a Page Up/Page Down para rolar as listas e áreas de texto (incluindo este console).","To maximise/minimise the console, press \"TC\".":"Para maximizar / minimizar o console, pressione \"TC\".","To enter command line mode, press \":\"":"Para entrar no modo de linha de comando, pressione \":\"","To exit command line mode, press ESCAPE":"Para sair do modo de linha de comando, pressione o ESC","For the complete list of available keyboard shortcuts, type `help shortcuts`":"Para a lista completa de atalhos de teclado disponíveis, digite `help shortcuts`","Imports an Evernote notebook file (.enex file).":"Importa um arquivo de caderno do Evernote (arquivo .enex).","Do not ask for confirmation.":"Não pedir confirmação.","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"O arquivo \"%s\" será importado para o caderno existente \"%s\". Continuar?","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"O novo caderno \"%s\" será criado e o arquivo \"%s\" será importado para ele. Continuar?","Found: %d.":"Encontrado: %d.","Created: %d.":"Criado: %d.","Updated: %d.":"Atualizado: %d.","Skipped: %d.":"Ignorado: %d.","Resources: %d.":"Recursos: %d.","Tagged: %d.":"Tag adicionada: %d.","Importing notes...":"Importando notas ...","The notes have been imported: %s":"As notas foram importadas: %s","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"Exibe as notas no caderno atual. Use `ls /` para exibir a lista de cadernos.","Displays only the first top notes.":"Exibe apenas as primeiras notas.","Sorts the item by (eg. title, updated_time, created_time).":"Classifica o item por (ex.: title, update_time, created_time).","Reverses the sorting order.":"Inverte a ordem de classificação.","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"Exibe apenas os itens do(s) tipo(s) específico(s). Pode ser `n` para notas,` t` para tarefas, ou `nt` para notas e tarefas (por exemplo.` -tt` exibiria apenas os itens pendentes, enquanto `-ttd` exibiria notas e tarefas .","Either \"text\" or \"json\"":"Ou \"text\" ou \"json\"","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"Use o formato da lista longa. O formato é ID, NOTE_COUNT (para caderno), DATE, TODO_CHECKED (para tarefas), TITLE","Please select a notebook first.":"Por favor, selecione um caderno primeiro.","Creates a new notebook.":"Cria um novo caderno.","Creates a new note.":"Cria uma nova nota.","Notes can only be created within a notebook.":"As notas só podem ser criadas dentro de um caderno.","Creates a new to-do.":"Cria uma nova tarefa.","Moves the notes matching to [notebook].":"Move as notas correspondentes para [caderno].","Renames the given (note or notebook) to .":"Renomeia o dado (nota ou caderno) para .","Deletes the given notebook.":"Exclui o caderno informado.","Deletes the notebook without asking for confirmation.":"Exclui o caderno sem pedir confirmação.","Delete notebook? All notes within this notebook will also be deleted.":"","Deletes the notes matching .":"Exclui as notas correspondentes ao padrão .","Deletes the notes without asking for confirmation.":"Exclui as notas sem pedir confirmação.","%d notes match this pattern. Delete them?":"%d notas correspondem a este padrão. Apagar todos?","Delete note?":"Apagar nota?","Searches for the given in all the notes.":"Procura o padrão em todas as notas.","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"Sets the property of the given to the given [value]. Possible properties are:\n\n%s","Displays summary about the notes and notebooks.":"Exibe sumário sobre as notas e cadernos.","Synchronises with remote storage.":"Sincroniza com o armazenamento remoto.","Sync to provided target (defaults to sync.target config value)":"Sincronizar para destino fornecido (p padrão é o valor de configuração sync.target)","Synchronisation is already in progress.":"A sincronização já está em andamento.","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"O arquivo de bloqueio já está ativo. Se você sabe que nenhuma sincronização está ocorrendo, você pode excluir o arquivo de bloqueio em \"%s\" e retomar a operação.","Authentication was not completed (did not receive an authentication token).":"A autenticação não foi concluída (não recebeu um token de autenticação).","Synchronisation target: %s (%s)":"Alvo de sincronização: %s (%s)","Cannot initialize synchroniser.":"Não é possível inicializar o sincronizador.","Starting synchronisation...":"Iniciando sincronização...","Cancelling... Please wait.":"Cancelando... Aguarde."," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":" pode ser \"add\", \"remove\" ou \"list\" para atribuir ou remover [tag] de [nota], ou para listar as notas associadas a [tag]. O comando `taglist` pode ser usado para listar todas as tags.","Invalid command: \"%s\"":"Comando inválido: \"%s\""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":" pode ser \"toggle\" ou \"clear\". Use \"toggle\" para alternar entre as tarefas entre o estado completo e incompleto (se o alvo for uma nota comum, ele será convertido em uma tarefa a fazer). Use \"clear\" para converter a tarefa em uma nota normal.","Marks a to-do as non-completed.":"Marca uma tarefa como não completada.","Switches to [notebook] - all further operations will happen within this notebook.":"Alterna para [caderno] - todas as operações adicionais acontecerão dentro deste caderno.","Displays version information":"Exibe informações da versão","%s %s (%s)":"%s %s (%s)","Enum":"Enum","Type: %s.":"Tipo: %s.","Possible values: %s.":"Valores possíveis: %s.","Default: %s":"Padrão: %s","Possible keys/values:":"Possíveis chaves/valores:","Fatal error:":"Erro fatal:","The application has been authorised - you may now close this browser tab.":"O aplicativo foi autorizado - agora você pode fechar esta guia do navegador.","The application has been successfully authorised.":"O aplicativo foi autorizado com sucesso.","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"Abra a seguinte URL no seu navegador para autenticar o aplicativo. O aplicativo criará um diretório em \"Apps/Joplin\" e somente lerá e gravará arquivos neste diretório. Não terá acesso a nenhum arquivo fora deste diretório nem a nenhum outro dado pessoal. Nenhum dado será compartilhado com terceiros.","Search:":"Procurar:","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"","File":"Arquivo","New note":"Nova nota","New to-do":"Nova tarefa","New notebook":"Novo caderno","Import Evernote notes":"Importar notas do Evernote","Evernote Export Files":"Arquivos de Exportação do Evernote","Quit":"Sair","Edit":"Editar","Copy":"Copiar","Cut":"Cortar","Paste":"Colar","Search in all the notes":"Pesquisar em todas as notas","Tools":"Ferramentas","Synchronisation status":"Synchronisation status","Options":"Opções","Help":"Ajuda","Website and documentation":"Website e documentação","About Joplin":"Sobre o Joplin","%s %s (%s, %s)":"%s %s (%s, %s)","OK":"OK","Cancel":"Cancelar","Notes and settings are stored in: %s":"","Save":"","Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?":"","Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.":"","Disable encryption":"","Enable encryption":"","Master Keys":"","Active":"","ID":"","Source":"","Created":"Created","Updated":"Updated","Password":"","Password OK":"","Note: Only one master key is going to be used for encryption (the one marked as \"active\"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.":"","Status":"Status","Encryption is:":"","Enabled":"Enabled","Disabled":"Desabilitado","Back":"Voltar","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"O novo caderno \"%s\" será criado e o arquivo \"%s\" será importado para ele","Please create a notebook first.":"Primeiro, crie um caderno.","Note title:":"Título da nota:","Please create a notebook first":"Primeiro, crie um caderno","To-do title:":"Título da tarefa:","Notebook title:":"Título do caderno:","Add or remove tags:":"Adicionar ou remover tags:","Separate each tag by a comma.":"Separe cada tag por vírgula.","Rename notebook:":"Renomear caderno:","Set alarm:":"Definir alarme:","Layout":"Layout","Some items cannot be synchronised.":"Some items cannot be synchronised.","View them now":"","Some items cannot be decrypted.":"Some items cannot be decrypted.","Set the password":"","Add or remove tags":"Adicionar ou remover tags","Switch between note and to-do type":"Alternar entre os tipos Nota e Tarefa","Delete":"Excluir","Delete notes?":"Excluir notas?","No notes in here. Create one by clicking on \"New note\".":"Não há notas aqui. Crie uma, clicando em \"Nova nota\".","There is currently no notebook. Create one by clicking on \"New notebook\".":"There is currently no notebook. Create one by clicking on \"New notebook\".","Unsupported link or message: %s":"Link ou mensagem não suportada: %s","Attach file":"Anexar arquivo","Set alarm":"Definir alarme","Refresh":"Atualizar","Clear":"Limpar (clear)","OneDrive Login":"Login no OneDrive","Import":"Importar","Synchronisation Status":"Synchronisation Status","Encryption Options":"","Remove this tag from all the notes?":"Remover esta tag de todas as notas?","Remove this search from the sidebar?":"Remover essa pesquisa da barra lateral?","Rename":"Renomear","Synchronise":"Sincronizar","Notebooks":"Cadernos","Tags":"Tags","Searches":"Pesquisas","Please select where the sync status should be exported to":"Please select where the sync status should be exported to","Usage: %s":"Uso: %s","Unknown flag: %s":"Flag desconhecido: %s","File system":"Sistema de arquivos","OneDrive":"OneDrive","OneDrive Dev (For testing only)":"OneDrive Dev (apenas para testes)","Unknown log level: %s":"Nível de log desconhecido: %s","Unknown level ID: %s":"Nível ID desconhecido: %s","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"Não é possível atualizar token: faltam dados de autenticação. Iniciar a sincronização novamente pode corrigir o problema.","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"Não foi possível sincronizar com o OneDrive.\n\nEste erro geralmente acontece ao usar o OneDrive for Business, que infelizmente não pode ser suportado.\n\nConsidere usar uma conta regular do OneDrive.","Cannot access %s":"Não é possível acessar %s","Created local items: %d.":"Itens locais criados: %d.","Updated local items: %d.":"Itens locais atualizados: %d.","Created remote items: %d.":"Itens remotos criados: %d.","Updated remote items: %d.":"Itens remotos atualizados: %d.","Deleted local items: %d.":"Itens locais excluídos: %d.","Deleted remote items: %d.":"Itens remotos excluídos: %d.","State: \"%s\".":"Estado: \"%s\".","Cancelling...":"Cancelando...","Completed: %s":"Completado: %s","Synchronisation is already in progress. State: %s":"Sincronização já em andamento. Estado: %s","Conflicts":"Conflitos","A notebook with this title already exists: \"%s\"":"Já existe caderno com este título: \"%s\"","Notebooks cannot be named \"%s\", which is a reserved title.":"Os cadernos não podem ser nomeados como\"%s\", que é um título reservado.","Untitled":"Sem título","This note does not have geolocation information.":"Esta nota não possui informações de geolocalização.","Cannot copy note to \"%s\" notebook":"Não é possível copiar a nota para o caderno \"%s\" ","Cannot move note to \"%s\" notebook":"Não é possível mover a nota para o caderno \"%s\"","Text editor":"Editor de texto","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"O editor que será usado para abrir uma nota. Se nenhum for indicado, ele tentará detectar automaticamente o editor padrão.","Language":"Idioma","Date format":"Formato de data","Time format":"Formato de hora","Theme":"Tema","Light":"Light","Dark":"Dark","Show uncompleted todos on top of the lists":"Mostrar tarefas incompletas no topo das listas","Save geo-location with notes":"Salvar geolocalização com notas","Synchronisation interval":"Intervalo de sincronização","%d minutes":"%d minutos","%d hour":"%d hora","%d hours":"%d horas","Automatically update the application":"Atualizar automaticamente o aplicativo","Show advanced options":"Mostrar opções avançadas","Synchronisation target":"Alvo de sincronização","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"O alvo para sincronizar. Se estiver sincronizando com o sistema de arquivos, configure `sync.2.path` para especificar o diretório de destino.","Directory to synchronise with (absolute path)":"","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"O caminho para sincronizar, quando a sincronização do sistema de arquivos está habilitada. Veja `sync.target`.","Invalid option value: \"%s\". Possible values are: %s.":"Valor da opção inválida: \"%s\". Os valores possíveis são: %s.","Items that cannot be synchronised":"","\"%s\": \"%s\"":"","Sync status (synced items / total items)":"Status de sincronização (sincronizados / totais)","%s: %d/%d":"%s: %d/%d","Total: %d/%d":"Total: %d/%d","Conflicted: %d":"Em conflito: %d","To delete: %d":"Para excluir: %d","Folders":"Pastas","%s: %d notes":"%s: %d notas","Coming alarms":"Próximos alarmes","On %s: %s":"Em %s: %s","There are currently no notes. Create one by clicking on the (+) button.":"Atualmente, não há notas. Crie uma, clicando no botão (+).","Delete these notes?":"Excluir estas notas?","Log":"Log","Export Debug Report":"Exportar Relatório de Debug","Configuration":"Configuração","Move to notebook...":"Mover para o caderno...","Move %d notes to notebook \"%s\"?":"Mover %d notas para o caderno \"%s\"?","Select date":"Selecionar data","Confirm":"Confirmar","Cancel synchronisation":"Cancelar sincronização","The notebook could not be saved: %s":"O caderno não pôde ser salvo: %s","Edit notebook":"Editar caderno","This note has been modified:":"Esta nota foi modificada:","Save changes":"Gravar alterações","Discard changes":"Descartar alterações","Unsupported image type: %s":"Tipo de imagem não suportada: %s","Attach photo":"Anexar foto","Attach any file":"Anexar qualquer arquivo","Convert to note":"Converter para nota","Convert to todo":"Converter para tarefa","Hide metadata":"Ocultar metadados","Show metadata":"Exibir metadados","View on map":"Ver no mapa","Delete notebook":"Excluir caderno","Login with OneDrive":"Login com OneDrive","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"Clique no botão (+) para criar uma nova nota ou caderno. Clique no menu lateral para acessar seus cadernos existentes.","You currently have no notebook. Create one by clicking on (+) button.":"Você não possui cadernos. Crie um clicando no botão (+).","Welcome":"Bem-vindo"} \ No newline at end of file diff --git a/ElectronClient/app/locales/ru_RU.json b/ElectronClient/app/locales/ru_RU.json new file mode 100644 index 000000000..c9a8e0020 --- /dev/null +++ b/ElectronClient/app/locales/ru_RU.json @@ -0,0 +1 @@ +{"Give focus to next pane":"Переключиться на следующую панель","Give focus to previous pane":"Переключиться на предыдущую панель","Enter command line mode":"Войти в режим командной строки","Exit command line mode":"Выйти из режима командной строки","Edit the selected note":"Редактировать выбранную заметку","Cancel the current command.":"Отменить текущую команду.","Exit the application.":"Выйти из приложения.","Delete the currently selected note or notebook.":"Удалить текущую выбранную заметку или блокнот.","To delete a tag, untag the associated notes.":"Чтобы удалить тег, уберите его с ассоциированных с ним заметок.","Please select the note or notebook to be deleted first.":"Сначала выберите заметку или блокнот, которые должны быть удалены.","Set a to-do as completed / not completed":"Отметить задачу как завершённую/незавершённую","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"[tc] переключить консоль между развёрнутой/свёрнутой/скрытой/видимой.","Search":"Поиск","[t]oggle note [m]etadata.":"[tm] переключить отображение метаданных заметки.","[M]ake a new [n]ote":"[mn] создать новую заметку","[M]ake a new [t]odo":"[mt] создать новую задачу","[M]ake a new note[b]ook":"[mb] создать новый блокнот","Copy ([Y]ank) the [n]ote to a notebook.":"[yn] копировать заметку в блокнот.","Move the note to a notebook.":"Переместить заметку в блокнот.","Press Ctrl+D or type \"exit\" to exit the application":"Для выхода из приложения нажмите Ctrl+D или введите «exit»","More than one item match \"%s\". Please narrow down your query.":"Более одного элемента соответствуют «%s». Уточните ваш запрос, пожалуйста.","No notebook selected.":"Не выбран блокнот.","No notebook has been specified.":"Не был указан блокнот.","Y":"Y","n":"n","N":"N","y":"y","Cancelling background synchronisation... Please wait.":"Отмена фоновой синхронизации... Пожалуйста, ожидайте.","No such command: %s":"Нет такой команды: %s","The command \"%s\" is only available in GUI mode":"Команда «%s» доступна только в режиме GUI","Missing required argument: %s":"Отсутствует требуемый аргумент: %s","%s: %s":"%s: %s","Your choice: ":"Ваш выбор:","Invalid answer: %s":"Неверный ответ: %s","Attaches the given file to the note.":"Прикрепляет заданный файл к заметке.","Cannot find \"%s\".":"Не удалось найти «%s».","Displays the given note.":"Отображает заданную заметку.","Displays the complete information about note.":"Отображает полную информацию о заметке.","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"Выводит или задаёт параметр конфигурации. Если [value] не указано, выведет значение [name]. Если не указаны ни [name], ни [value], выведет текущую конфигурацию.","Also displays unset and hidden config variables.":"Также выводит неустановленные или скрытые переменные конфигурации.","%s = %s (%s)":"%s = %s (%s)","%s = %s":"%s = %s","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"Дублирует заметки, содержащие , в [notebook]. Если блокнот не указан, заметки продублируются в текущем.","Marks a to-do as done.":"Отмечает задачу как завершённую.","Note is not a to-do: \"%s\"":"Заметка не является задачей: «%s»","Edit note.":"Редактировать заметку.","No text editor is defined. Please set it using `config editor `":"Текстовый редактор не определён. Задайте его, используя `config editor `","No active notebook.":"Нет активного блокнота.","Note does not exist: \"%s\". Create it?":"Заметки не существует: «%s». Создать?","Starting to edit note. Close the editor to get back to the prompt.":"Запуск редактирования заметки. Закройте редактор, чтобы вернуться к командной строке.","Note has been saved.":"Заметка сохранена.","Exits the application.":"Выход из приложения.","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"Экспортирует данные Joplin в заданный каталог. По умолчанию экспортируется полная база данных, включая блокноты, заметки, теги и ресурсы.","Exports only the given note.":"Экспортирует только заданную заметку.","Exports only the given notebook.":"Экспортирует только заданный блокнот.","Displays a geolocation URL for the note.":"Выводит URL геолокации для заметки.","Displays usage information.":"Выводит информацию об использовании.","Shortcuts are not available in CLI mode.":"Ярлыки недоступны в режиме командной строки.","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"Введите `help [команда]` для получения информации о команде или `help all` для получения полной информации по использованию.","The possible commands are:":"Доступные команды:","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"В любой команде можно ссылаться на заметку или блокнот по названию или ID, либо используя ярлыки `$n` или `$b`, указывающие на текущую заметку или блокнот соответственно. С помощью `$c` можно ссылаться на текущий выбранный элемент.","To move from one pane to another, press Tab or Shift+Tab.":"Чтобы переключаться между панелями, нажимайте Tab или Shift+Tab","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"Используйте стрелки и клавиши перелистывания страницы вверх/вниз для прокрутки списков и текстовых областей (включая эту консоль).","To maximise/minimise the console, press \"TC\".":"Чтобы развернуть/свернуть консоль, нажимайте «TC».","To enter command line mode, press \":\"":"Чтобы войти в режим командной строки, нажмите «:»","To exit command line mode, press ESCAPE":"Чтобы выйти из режима командной строки, нажмите ESCAPE","For the complete list of available keyboard shortcuts, type `help shortcuts`":"Для просмотра списка доступных клавиатурных сочетаний введите `help shortcuts`.","Imports an Evernote notebook file (.enex file).":"Импортирует файл блокнотов Evernote (.enex-файл).","Do not ask for confirmation.":"Не запрашивать подтверждение.","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"Файл «%s» будет импортирован в существующий блокнот «%s». Продолжить?","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"Будет создан новый блокнот «%s» и в него будет импортирован файл «%s». Продолжить?","Found: %d.":"Найдено: %d.","Created: %d.":"Создано: %d.","Updated: %d.":"Обновлено: %d.","Skipped: %d.":"Пропущено: %d.","Resources: %d.":"Ресурсов: %d.","Tagged: %d.":"С тегами: %d.","Importing notes...":"Импорт заметок...","The notes have been imported: %s":"Импортировано заметок: %s","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"Выводит заметки текущего блокнота. Используйте `ls /` для вывода списка блокнотов.","Displays only the first top notes.":"Выводит только первые заметок.","Sorts the item by (eg. title, updated_time, created_time).":"Сортирует элементы по (например, title, updated_time, created_time).","Reverses the sorting order.":"Обращает порядок сортировки.","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"Выводит только элементы указанного типа. Может быть `n` для заметок, `t` для задач или `nt` для заметок и задач (например, `-tt` выведет только задачи, в то время как `-ttd` выведет заметки и задачи).","Either \"text\" or \"json\"":"«text» или «json»","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"Использовать формат длинного списка. Форматом является ID, NOTE_COUNT (для блокнотов), DATE, TODO_CHECKED (для задач), TITLE","Please select a notebook first.":"Сначала выберите блокнот.","Creates a new notebook.":"Создаёт новый блокнот.","Creates a new note.":"Создаёт новую заметку.","Notes can only be created within a notebook.":"Заметки могут быть созданы только в блокноте.","Creates a new to-do.":"Создаёт новую задачу.","Moves the notes matching to [notebook].":"Перемещает заметки, содержащие в [notebook].","Renames the given (note or notebook) to .":"Переименовывает заданный (заметку или блокнот) в .","Deletes the given notebook.":"Удаляет заданный блокнот.","Deletes the notebook without asking for confirmation.":"Удаляет блокнот без запроса подтверждения.","Delete notebook? All notes within this notebook will also be deleted.":"Удалить блокнот? Все заметки в этом блокноте также будут удалены.","Deletes the notes matching .":"Удаляет заметки, соответствующие .","Deletes the notes without asking for confirmation.":"Удаляет заметки без запроса подтверждения.","%d notes match this pattern. Delete them?":"%d заметок соответствуют этому шаблону. Удалить их?","Delete note?":"Удалить заметку?","Searches for the given in all the notes.":"Запросы для заданного во всех заметках.","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"Устанавливает для свойства заданной заданное [value]. Возможные свойства:\n\n%s","Displays summary about the notes and notebooks.":"Выводит общую информацию о заметках и блокнотах.","Synchronises with remote storage.":"Синхронизирует с удалённым хранилищем.","Sync to provided target (defaults to sync.target config value)":"Синхронизация с заданной целью (по умолчанию — значение конфигурации sync.target)","Synchronisation is already in progress.":"Синхронизация уже выполняется.","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"Файл блокировки уже установлен. Если вам известно, что синхронизация не производится, вы можете удалить файл блокировки в «%s» и возобновить операцию.","Authentication was not completed (did not receive an authentication token).":"Аутентификация не была завершена (не получен токен аутентификации).","Synchronisation target: %s (%s)":"Цель синхронизации: %s (%s)","Cannot initialize synchroniser.":"Не удалось инициировать синхронизацию.","Starting synchronisation...":"Начало синхронизации...","Cancelling... Please wait.":"Отмена... Пожалуйста, ожидайте."," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":" может быть «add», «remove» или «list», чтобы назначить или убрать [tag] с [note], или чтобы вывести список заметок, ассоциированых с [tag]. Команда `tag list` может быть использована для вывода списка всех тегов.","Invalid command: \"%s\"":"Неверная команда: «%s»"," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":" может быть «toggle» или «clear». «toggle» используется для переключения статуса заданной задачи на завершённую или незавершённую (если применить к обычной заметке, она будет преобразована в задачу). «clear» используется для преобразования задачи обратно в обычную заметку.","Marks a to-do as non-completed.":"Отмечает задачу как незавершённую.","Switches to [notebook] - all further operations will happen within this notebook.":"Переключает на [блокнот] — все дальнейшие операции будут происходить в этом блокноте.","Displays version information":"Выводит информацию о версии","%s %s (%s)":"%s %s (%s)","Enum":"Enum","Type: %s.":"Тип: %s.","Possible values: %s.":"Возможные значения: %s.","Default: %s":"По умолчанию: %s","Possible keys/values:":"Возможные ключи/значения:","Fatal error:":"Фатальная ошибка:","The application has been authorised - you may now close this browser tab.":"Приложение авторизовано — можно закрыть вкладку браузера.","The application has been successfully authorised.":"Приложение успешно авторизовано.","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"Откройте следующую ссылку в вашем браузере для аутентификации приложения. Приложением будет создан каталог «Apps/Joplin». Чтение и запись файлов будет осуществляться только в его пределах. У приложения не будет доступа к каким-либо файлам за пределами этого каталога и другим личным данным. Никакая информация не будет передана третьим лицам.","Search:":"Поиск:","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"Добро пожаловать в Joplin!\n\nВведите `:help shortcuts` для просмотра списка клавиатурных сочетаний или просто `:help` для просмотра информации об использовании.\n\nНапример, для создания блокнота нужно ввести `mb`, для создания заметки — `mn`.","File":"Файл","New note":"Новая заметка","New to-do":"Новая задача","New notebook":"Новый блокнот","Import Evernote notes":"Импортировать заметки из Evernote","Evernote Export Files":"Файлы экспорта Evernote","Quit":"Выход","Edit":"Редактировать","Copy":"Копировать","Cut":"Вырезать","Paste":"Вставить","Search in all the notes":"Поиск во всех заметках","Tools":"Инструменты","Synchronisation status":"Статус синхронизации","Options":"Настройки","Help":"Помощь","Website and documentation":"Сайт и документация","About Joplin":"О Joplin","%s %s (%s, %s)":"%s %s (%s, %s)","OK":"OK","Cancel":"Отмена","Notes and settings are stored in: %s":"Заметки и настройки сохранены в: %s","Save":"Сохранить","Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?":"","Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.":"","Disable encryption":"","Enable encryption":"","Master Keys":"","Active":"","ID":"ID","Source":"Источник","Created":"Создана","Updated":"Обновлена","Password":"","Password OK":"","Note: Only one master key is going to be used for encryption (the one marked as \"active\"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.":"","Status":"Статус","Encryption is:":"","Enabled":"Enabled","Disabled":"Отключена","Back":"Назад","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"Будет создан новый блокнот «%s» и в него будет импортирован файл «%s»","Please create a notebook first.":"Сначала создайте блокнот.","Note title:":"Название заметки:","Please create a notebook first":"Сначала создайте блокнот","To-do title:":"Название задачи:","Notebook title:":"Название блокнота:","Add or remove tags:":"Добавить или удалить теги:","Separate each tag by a comma.":"Каждый тег отделяется запятой.","Rename notebook:":"Переименовать блокнот:","Set alarm:":"Установить напоминание:","Layout":"Вид","Some items cannot be synchronised.":"Некоторые элементы не могут быть синхронизированы.","View them now":"Просмотреть их сейчас","Some items cannot be decrypted.":"Some items cannot be decrypted.","Set the password":"","Add or remove tags":"Добавить или удалить теги","Switch between note and to-do type":"Переключить тип между заметкой и задачей","Delete":"Удалить","Delete notes?":"Удалить заметки?","No notes in here. Create one by clicking on \"New note\".":"Здесь нет заметок. Создайте новую нажатием на «Новая заметка».","There is currently no notebook. Create one by clicking on \"New notebook\".":"Сейчас здесь нет блокнотов. Создайте новый нажав «Новый блокнот».","Unsupported link or message: %s":"Неподдерживаемая ссыка или сообщение: %s","Attach file":"Прикрепить файл","Set alarm":"Установить напоминание","Refresh":"Обновить","Clear":"Очистить","OneDrive Login":"Вход в OneDrive","Import":"Импорт","Synchronisation Status":"Статус синхронизации","Encryption Options":"","Remove this tag from all the notes?":"Убрать этот тег со всех заметок?","Remove this search from the sidebar?":"Убрать этот запрос с боковой панели?","Rename":"Переименовать","Synchronise":"Синхронизировать","Notebooks":"Блокноты","Tags":"Теги","Searches":"Запросы","Please select where the sync status should be exported to":"Выберите, куда должен быть экспортирован статус синхронизации","Usage: %s":"Использование: %s","Unknown flag: %s":"Неизвестный флаг: %s","File system":"Файловая система","OneDrive":"OneDrive","OneDrive Dev (For testing only)":"OneDrive Dev (только для тестирования)","Unknown log level: %s":"Неизвестный уровень лога: %s","Unknown level ID: %s":"Неизвестный ID уровня: %s","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"Не удалось обновить токен: отсутствуют данные аутентификации. Повторный запуск синхронизации может решить проблему.","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"Не удалось синхронизироваться с OneDrive.\n\nТакая ошибка часто возникает при использовании OneDrive для бизнеса, который, к сожалению, не поддерживается.\n\nПожалуйста, рассмотрите возможность использования обычного аккаунта OneDrive.","Cannot access %s":"Не удалось получить доступ %s","Created local items: %d.":"Создано локальных элементов: %d.","Updated local items: %d.":"Обновлено локальных элементов: %d.","Created remote items: %d.":"Создано удалённых элементов: %d.","Updated remote items: %d.":"Обновлено удалённых элементов: %d.","Deleted local items: %d.":"Удалено локальных элементов: %d.","Deleted remote items: %d.":"Удалено удалённых элементов: %d.","State: \"%s\".":"Статус: «%s».","Cancelling...":"Отмена...","Completed: %s":"Завершено: %s","Synchronisation is already in progress. State: %s":"Синхронизация уже выполняется. Статус: %s","Conflicts":"Конфликты","A notebook with this title already exists: \"%s\"":"Блокнот с таким названием уже существует: «%s»","Notebooks cannot be named \"%s\", which is a reserved title.":"Блокнот не может быть назван «%s», это зарезервированное название.","Untitled":"Без имени","This note does not have geolocation information.":"Эта заметка не содержит информации о геолокации.","Cannot copy note to \"%s\" notebook":"Не удалось скопировать заметку в блокнот «%s»","Cannot move note to \"%s\" notebook":"Не удалось переместить заметку в блокнот «%s»","Text editor":"Текстовый редактор","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"Редактор, в котором будут открываться заметки. Если не задан, будет произведена попытка автоматического определения редактора по умолчанию.","Language":"Язык","Date format":"Формат даты","Time format":"Формат времени","Theme":"Тема","Light":"Светлая","Dark":"Тёмная","Show uncompleted todos on top of the lists":"Показывать незавершённые задачи вверху списков","Save geo-location with notes":"Сохранять информацию о геолокации в заметках","Synchronisation interval":"Интервал синхронизации","%d minutes":"%d минут","%d hour":"%d час","%d hours":"%d часов","Automatically update the application":"Автоматически обновлять приложение","Show advanced options":"Показывать расширенные настройки","Synchronisation target":"Цель синхронизации","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"То, с чем будет осуществляться синхронизация. При синхронизации с файловой системой в `sync.2.path` указывается целевой каталог.","Directory to synchronise with (absolute path)":"Каталог синхронизации (абсолютный путь)","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"Путь для синхронизации при включённой синхронизации с файловой системой. См. `sync.target`.","Invalid option value: \"%s\". Possible values are: %s.":"Неверное значение параметра: «%s». Доступные значения: %s.","Items that cannot be synchronised":"Элементы, которые не могут быть синхронизированы","\"%s\": \"%s\"":"«%s»: «%s»","Sync status (synced items / total items)":"Статус синхронизации (элементов синхронизировано/всего)","%s: %d/%d":"%s: %d/%d","Total: %d/%d":"Всего: %d/%d","Conflicted: %d":"Конфликтующих: %d","To delete: %d":"К удалению: %d","Folders":"Папки","%s: %d notes":"%s: %d заметок","Coming alarms":"Грядущие напоминания","On %s: %s":"В %s: %s","There are currently no notes. Create one by clicking on the (+) button.":"Сейчас здесь нет заметок. Создаёте новую, нажав кнопку (+).","Delete these notes?":"Удалить эти заметки?","Log":"Лог","Export Debug Report":"Экспортировать отладочный отчёт","Configuration":"Конфигурация","Move to notebook...":"Переместить в блокнот...","Move %d notes to notebook \"%s\"?":"Переместить %d заметок в блокнот «%s»?","Select date":"Выбрать дату","Confirm":"Подтвердить","Cancel synchronisation":"Отменить синхронизацию","The notebook could not be saved: %s":"Не удалось сохранить блокнот: %s","Edit notebook":"Редактировать блокнот","This note has been modified:":"Эта заметка была изменена:","Save changes":"Сохранить изменения","Discard changes":"Отменить изменения","Unsupported image type: %s":"Неподдерживаемый формат изображения: %s","Attach photo":"Прикрепить фото","Attach any file":"Прикрепить любой файл","Convert to note":"Преобразовать в заметку","Convert to todo":"Преобразовать в задачу","Hide metadata":"Скрыть метаданные","Show metadata":"Показать метаданные","View on map":"Посмотреть на карте","Delete notebook":"Удалить блокнот","Login with OneDrive":"Войти в OneDrive","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"Нажмите на кнопку (+) для создания новой заметки или нового блокнота. Нажмите на боковое меню для доступа к вашим существующим блокнотам.","You currently have no notebook. Create one by clicking on (+) button.":"У вас сейчас нет блокнота. Создайте его нажатием на кнопку (+).","Welcome":"Добро пожаловать"} \ No newline at end of file diff --git a/ElectronClient/app/locales/zh_CN.json b/ElectronClient/app/locales/zh_CN.json new file mode 100644 index 000000000..556940192 --- /dev/null +++ b/ElectronClient/app/locales/zh_CN.json @@ -0,0 +1 @@ +{"Give focus to next pane":"聚焦于下个面板","Give focus to previous pane":"聚焦于上个面板","Enter command line mode":"进入命令行模式","Exit command line mode":"退出命令行模式","Edit the selected note":"编辑所选笔记","Cancel the current command.":"取消当前命令。","Exit the application.":"退出程序。","Delete the currently selected note or notebook.":"删除当前所选笔记或笔记本。","To delete a tag, untag the associated notes.":"移除相关笔记的标签后才可删除此标签。","Please select the note or notebook to be deleted first.":"请选择最先删除的笔记或笔记本。","Set a to-do as completed / not completed":"设置待办事项为已完成或未完成","[t]oggle [c]onsole between maximized/minimized/hidden/visible.":"在最大化/最小化/隐藏/显示间切换[t]控制台[c]。","Search":"搜索","[t]oggle note [m]etadata.":"切换[t]笔记元数据[m]。","[M]ake a new [n]ote":"创建[M]新笔记[n]","[M]ake a new [t]odo":"创建[M]新待办事项[t]","[M]ake a new note[b]ook":"创建[M]新笔记本[b]","Copy ([Y]ank) the [n]ote to a notebook.":"复制[Y]笔记[n]至笔记本。","Move the note to a notebook.":"移动笔记至笔记本。","Press Ctrl+D or type \"exit\" to exit the application":"按Ctrl+D或输入\"exit\"退出程序","More than one item match \"%s\". Please narrow down your query.":"有多个项目与\"%s\"匹配,请缩小您的查询范围。","No notebook selected.":"未选择笔记本。","No notebook has been specified.":"无指定笔记本。","Y":"是","n":"否","N":"否","y":"是","Cancelling background synchronisation... Please wait.":"正在取消背景同步...请稍后。","No such command: %s":"无以下命令:%s","The command \"%s\" is only available in GUI mode":"命令\"%s\"仅在GUI模式下可用","Missing required argument: %s":"缺失所需参数:%s","%s: %s":"%s: %s","Your choice: ":"您的选择: ","Invalid answer: %s":"此答案无效:%s","Attaches the given file to the note.":"给笔记附加给定文件。","Cannot find \"%s\".":"无法找到 \"%s\"。","Displays the given note.":"显示给定笔记。","Displays the complete information about note.":"显示关于笔记的全部信息。","Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.":"获取或设置配置变量。若未提供[value],则会显示[name]的值。若[name]及[value]都未提供,则列出当前配置。","Also displays unset and hidden config variables.":"同时显示未设置的与隐藏的配置变量。","%s = %s (%s)":"%s = %s (%s)","%s = %s":"%s = %s","Duplicates the notes matching to [notebook]. If no notebook is specified the note is duplicated in the current notebook.":"复制符合的笔记至[notebook]。若无指定笔记本则在当前笔记本内复制该笔记。","Marks a to-do as done.":"标记待办事项为完成。","Note is not a to-do: \"%s\"":"笔记非待办事项:\"%s\"","Edit note.":"编辑笔记。","No text editor is defined. Please set it using `config editor `":"未定义文本编辑器。请通过 `config editor `设置。","No active notebook.":"无活动笔记本。","Note does not exist: \"%s\". Create it?":"此笔记不存在:\"%s\"。是否创建?","Starting to edit note. Close the editor to get back to the prompt.":"开始编辑笔记。关闭编辑器则返回提示。","Note has been saved.":"笔记已被保存。","Exits the application.":"退出程序。","Exports Joplin data to the given directory. By default, it will export the complete database including notebooks, notes, tags and resources.":"导出Joplin数据至给定文件目录。默认为导出所有的数据库,包含笔记本、笔记、标签及资源。","Exports only the given note.":"仅导出给定笔记。","Exports only the given notebook.":"仅导出给定笔记本。","Displays a geolocation URL for the note.":"显示此笔记的地理定位URL地址。","Displays usage information.":"显示使用信息。","Shortcuts are not available in CLI mode.":"快捷键在CLI模式下不可用。","Type `help [command]` for more information about a command; or type `help all` for the complete usage information.":"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.","The possible commands are:":"可用命令为:","In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.":"在任意命令中,笔记或笔记本可通过其标题或ID来引用,也可使用代表当前所选笔记或笔记本的变量`$n`与`$b`。`$c`可用于引用当前所选项目。","To move from one pane to another, press Tab or Shift+Tab.":"按Tab或Shift+Tab切换面板。","Use the arrows and page up/down to scroll the lists and text areas (including this console).":"通过上下左右与page up/down键来滚动列表与文本区域(包含此控制台)。","To maximise/minimise the console, press \"TC\".":"按\"TC\"最大化/最小化控制台。","To enter command line mode, press \":\"":"按\":\"键进入命令行模式","To exit command line mode, press ESCAPE":"按ESC键退出命令行模式","For the complete list of available keyboard shortcuts, type `help shortcuts`":"输入`help shortcuts`显示全部可用的快捷键列表。","Imports an Evernote notebook file (.enex file).":"导入Evernote笔记本文件(.enex文件)。","Do not ask for confirmation.":"不再要求确认。","File \"%s\" will be imported into existing notebook \"%s\". Continue?":"文件\"%s\"将会被导入至现有笔记本\"%s\"。是否继续?","New notebook \"%s\" will be created and file \"%s\" will be imported into it. Continue?":"将创建新笔记本\"%s\"并将文件\"%s\"导入至其中。是否继续?","Found: %d.":"已找到:%d条。","Created: %d.":"已创建:%d条。","Updated: %d.":"已更新:%d条。","Skipped: %d.":"已跳过:%d条。","Resources: %d.":"资源:%d。","Tagged: %d.":"已标签:%d条。","Importing notes...":"正在导入笔记...","The notes have been imported: %s":"以下笔记已被导入:%s","Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.":"显示当前笔记本的笔记。使用`ls /`显示笔记本列表。","Displays only the first top notes.":"只显示最上方的条笔记。","Sorts the item by (eg. title, updated_time, created_time).":"使用排序项目(例标题、更新日期、创建日期)。","Reverses the sorting order.":"反转排序顺序。","Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.":"仅显示指定格式的项目。`n`代表笔记,`t`代表待办事项,`nt`代表笔记和待办事项(例,`-tt`则会仅显示待办事项,`-ttd`则会显示笔记和待办事项)。","Either \"text\" or \"json\"":"\"文本\"或\"json\"","Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE":"使用长列表格式。格式为ID, NOTE_COUNT(笔记本), DATE, TODO_CHECKED(待办事项),TITLE","Please select a notebook first.":"请先选择笔记本。","Creates a new notebook.":"创建新笔记本。","Creates a new note.":"创建新笔记。","Notes can only be created within a notebook.":"笔记只能创建于笔记本内。","Creates a new to-do.":"创建新待办事项。","Moves the notes matching to [notebook].":"移动符合的笔记至[notebook]。","Renames the given (note or notebook) to .":"重命名给定的(笔记或笔记本)至。","Deletes the given notebook.":"删除给定笔记本。","Deletes the notebook without asking for confirmation.":"删除笔记本(不要求确认)。","Delete notebook? All notes within this notebook will also be deleted.":"","Deletes the notes matching .":"删除符合的笔记。","Deletes the notes without asking for confirmation.":"删除笔记(不要求确认)。","%d notes match this pattern. Delete them?":"%d条笔记符合此模式。是否删除它们?","Delete note?":"是否删除笔记?","Searches for the given in all the notes.":"在所有笔记内搜索给定的。","Sets the property of the given to the given [value]. Possible properties are:\n\n%s":"Sets the property of the given to the given [value]. Possible properties are:\n\n%s","Displays summary about the notes and notebooks.":"显示关于笔记与笔记本的概况。","Synchronises with remote storage.":"与远程储存空间同步。","Sync to provided target (defaults to sync.target config value)":"同步至所提供的目标(默认为同步目标配置值)","Synchronisation is already in progress.":"同步正在进行中。","Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.":"锁定文件已被保留。若当前没有任何正在进行的同步,您可以在\"%s\"删除锁定文件并继续操作。","Authentication was not completed (did not receive an authentication token).":"认证未完成(未收到认证令牌)。","Synchronisation target: %s (%s)":"同步目标:%s (%s)","Cannot initialize synchroniser.":"无法初始化同步。","Starting synchronisation...":"开始同步...","Cancelling... Please wait.":"正在取消...请稍后。"," can be \"add\", \"remove\" or \"list\" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.":"可添加\"add\"、删除\"remove\",或列出\"list\"于[note],用来指定或移除[tag],也可以列出于[tag]相关的笔记。`tag list`命令可用于列出所有标签。","Invalid command: \"%s\"":"无效命令:\"%s\""," can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.":"可被切换\"toggle\"或清除\"clear\"。用\"toggle\"可使给定待办事项在已完成与未完成两个状态下切换(若目标为常规笔记,它将被转换成待办事项)。用\"clear\"可把该待办事项转换回常规笔记。","Marks a to-do as non-completed.":"标记待办事项为未完成。","Switches to [notebook] - all further operations will happen within this notebook.":"切换至[notebook] - 所有进一步处理将在此笔记本中进行。","Displays version information":"显示版本信息。","%s %s (%s)":"%s %s (%s)","Enum":"枚举","Type: %s.":"格式:%s。","Possible values: %s.":"可用值: %s。","Default: %s":"默认值: %s","Possible keys/values:":"可用键/值:","Fatal error:":"严重错误:","The application has been authorised - you may now close this browser tab.":"此程序已被授权 - 您可以关闭此浏览页面了。","The application has been successfully authorised.":"此程序已被成功授权。","Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.":"请用网页浏览器打开以下URL来认证此程序。此程序将创建\"Apps/Joplin\"目录,并仅在此目录内写入及读取文件。程序对于在该目录外的文件或任何个人数据没有任何访问权限。同时也不会与第三方共享任何数据。","Search:":"搜索:","Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.":"","File":"文件","New note":"新笔记","New to-do":"新待办事项","New notebook":"新笔记本","Import Evernote notes":"导入Evernote笔记","Evernote Export Files":"Evernote导出文件","Quit":"退出","Edit":"编辑","Copy":"复制","Cut":"剪切","Paste":"粘贴","Search in all the notes":"在所有笔记内搜索","Tools":"工具","Synchronisation status":"同步状态","Options":"选项","Help":"帮助","Website and documentation":"网站与文档","About Joplin":"关于Joplin","%s %s (%s, %s)":"%s %s (%s, %s)","OK":"确认","Cancel":"取消","Notes and settings are stored in: %s":"","Save":"","Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?":"","Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.":"","Disable encryption":"","Enable encryption":"","Master Keys":"","Active":"","ID":"","Source":"","Created":"Created","Updated":"Updated","Password":"","Password OK":"","Note: Only one master key is going to be used for encryption (the one marked as \"active\"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.":"","Status":"状态","Encryption is:":"","Enabled":"Enabled","Disabled":"已禁止","Back":"返回","New notebook \"%s\" will be created and file \"%s\" will be imported into it":"将创建新笔记本\"%s\"并将文件\"%s\"导入至其中","Please create a notebook first.":"请先创建笔记本。","Note title:":"笔记标题:","Please create a notebook first":"请先创建笔记本","To-do title:":"待办事项标题:","Notebook title:":"笔记本标题:","Add or remove tags:":"添加或删除标签:","Separate each tag by a comma.":"用逗号\",\"分开每个标签。","Rename notebook:":"重命名笔记本:","Set alarm:":"设置提醒:","Layout":"布局","Some items cannot be synchronised.":"一些项目无法被同步。","View them now":"马上查看","Some items cannot be decrypted.":"Some items cannot be decrypted.","Set the password":"","Add or remove tags":"添加或删除标签","Switch between note and to-do type":"在笔记和待办事项类型之间切换","Delete":"删除","Delete notes?":"是否删除笔记?","No notes in here. Create one by clicking on \"New note\".":"此处无笔记。点击\"新笔记\"创建新笔记。","There is currently no notebook. Create one by clicking on \"New notebook\".":"There is currently no notebook. Create one by clicking on \"New notebook\".","Unsupported link or message: %s":"不支持的链接或信息:%s","Attach file":"附加文件","Set alarm":"设置提醒","Refresh":"刷新","Clear":"清除","OneDrive Login":"登陆OneDrive","Import":"导入","Synchronisation Status":"同步状态","Encryption Options":"","Remove this tag from all the notes?":"从所有笔记中删除此标签?","Remove this search from the sidebar?":"从侧栏中删除此项搜索历史?","Rename":"重命名","Synchronise":"同步","Notebooks":"笔记本","Tags":"标签","Searches":"搜索历史","Please select where the sync status should be exported to":"Please select where the sync status should be exported to","Usage: %s":"使用:%s","Unknown flag: %s":"未知标记:%s","File system":"文件系统","OneDrive":"OneDrive","OneDrive Dev (For testing only)":"OneDrive开发员(仅测试用)","Unknown log level: %s":"未知日志level:%s","Unknown level ID: %s":"未知 level ID:%s","Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.":"无法刷新令牌:缺失认证数据。请尝试重新启动同步。","Could not synchronize with OneDrive.\n\nThis error often happens when using OneDrive for Business, which unfortunately cannot be supported.\n\nPlease consider using a regular OneDrive account.":"无法与OneDrive同步。\n\n此错误经常在使用OneDrive for Business时出现。很可惜我们无法支持此服务。\n\n请您考虑使用常规的OneDrive账号。","Cannot access %s":"无法访问%s","Created local items: %d.":"已新建本地项目: %d。","Updated local items: %d.":"已更新本地项目: %d。","Created remote items: %d.":"已新建远程项目: %d。","Updated remote items: %d.":"已更新远程项目: %d。","Deleted local items: %d.":"已删除本地项目: %d。","Deleted remote items: %d.":"已删除远程项目: %d。","State: \"%s\".":"状态:\"%s\"。","Cancelling...":"正在取消...","Completed: %s":"已完成:\"%s\"","Synchronisation is already in progress. State: %s":"同步正在进行中。状态:%s","Conflicts":"冲突","A notebook with this title already exists: \"%s\"":"以此标题命名的笔记本已存在:\"%s\"","Notebooks cannot be named \"%s\", which is a reserved title.":"笔记本无法被命名为\"%s\",此标题为保留标题。","Untitled":"无标题","This note does not have geolocation information.":"此笔记不包含地理定位信息。","Cannot copy note to \"%s\" notebook":"无法复制笔记至\"%s\"笔记本","Cannot move note to \"%s\" notebook":"无法移动笔记至\"%s\"笔记本","Text editor":"文本编辑器","The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.":"将用于打开笔记的编辑器。若未提供,将自动尝试检测默认编辑器。","Language":"语言","Date format":"日期格式","Time format":"时间格式","Theme":"主题","Light":"浅色","Dark":"深色","Show uncompleted todos on top of the lists":"在列表上方显示未完成的待办事项","Save geo-location with notes":"保存笔记时同时保存地理定位信息","Synchronisation interval":"同步间隔","%d minutes":"%d分","%d hour":"%d小时","%d hours":"%d小时","Automatically update the application":"自动更新此程序","Show advanced options":"显示高级选项","Synchronisation target":"同步目标","The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.":"同步的目标。若与文件系统同步,设置`sync.2.path`为指定目标目录。","Directory to synchronise with (absolute path)":"","The path to synchronise with when file system synchronisation is enabled. See `sync.target`.":"当文件系统同步开启时的同步路径。参考`sync.target`。","Invalid option value: \"%s\". Possible values are: %s.":"无效的选项值:\"%s\"。可用值为:%s。","Items that cannot be synchronised":"项目无法被同步。","\"%s\": \"%s\"":"\"%s\": \"%s\"","Sync status (synced items / total items)":"同步状态(已同步项目/项目总数)","%s: %d/%d":"%s:%d/%d条","Total: %d/%d":"总数:%d/%d条","Conflicted: %d":"有冲突的:%d条","To delete: %d":"将删除:%d条","Folders":"文件夹","%s: %d notes":"%s: %d条笔记","Coming alarms":"临近提醒","On %s: %s":"%s:%s","There are currently no notes. Create one by clicking on the (+) button.":"当前无笔记。点击(+)创建新笔记。","Delete these notes?":"是否删除这些笔记?","Log":"日志","Export Debug Report":"导出调试报告","Configuration":"配置","Move to notebook...":"移动至笔记本...","Move %d notes to notebook \"%s\"?":"移动%d条笔记至笔记本\"%s\"?","Select date":"选择日期","Confirm":"确认","Cancel synchronisation":"取消同步","The notebook could not be saved: %s":"此笔记本无法保存:%s","Edit notebook":"编辑笔记本","This note has been modified:":"此笔记已被修改:","Save changes":"保存更改","Discard changes":"放弃更改","Unsupported image type: %s":"不支持的图片格式:%s","Attach photo":"附加照片","Attach any file":"附加任何文件","Convert to note":"转换至笔记","Convert to todo":"转换至待办事项","Hide metadata":"隐藏元数据","Show metadata":"显示元数据","View on map":"查看地图","Delete notebook":"删除笔记本","Login with OneDrive":"用OneDrive登陆","Click on the (+) button to create a new note or notebook. Click on the side menu to access your existing notebooks.":"点击(+)按钮创建新笔记或笔记本。点击侧边菜单来访问您现有的笔记本。","You currently have no notebook. Create one by clicking on (+) button.":"您当前没有任何笔记本。点击(+)按钮创建新笔记本。","Welcome":"欢迎"} \ No newline at end of file diff --git a/ElectronClient/app/main-html.js b/ElectronClient/app/main-html.js index f601dd82c..bc9560215 100644 --- a/ElectronClient/app/main-html.js +++ b/ElectronClient/app/main-html.js @@ -4,21 +4,24 @@ require('app-module-path').addPath(__dirname); const { app } = require('./app.js'); -const { Folder } = require('lib/models/folder.js'); -const { Resource } = require('lib/models/resource.js'); -const { BaseItem } = require('lib/models/base-item.js'); -const { Note } = require('lib/models/note.js'); -const { Tag } = require('lib/models/tag.js'); -const { NoteTag } = require('lib/models/note-tag.js'); -const { Setting } = require('lib/models/setting.js'); +const Folder = require('lib/models/Folder.js'); +const Resource = require('lib/models/Resource.js'); +const BaseItem = require('lib/models/BaseItem.js'); +const Note = require('lib/models/Note.js'); +const Tag = require('lib/models/Tag.js'); +const NoteTag = require('lib/models/NoteTag.js'); +const MasterKey = require('lib/models/MasterKey'); +const Setting = require('lib/models/Setting.js'); const { Logger } = require('lib/logger.js'); const { FsDriverNode } = require('lib/fs-driver-node.js'); const { shimInit } = require('lib/shim-init-node.js'); +const EncryptionService = require('lib/services/EncryptionService'); const { bridge } = require('electron').remote.require('./bridge'); const fsDriver = new FsDriverNode(); Logger.fsDriver_ = fsDriver; Resource.fsDriver_ = fsDriver; +EncryptionService.fsDriver_ = fsDriver; // That's not good, but it's to avoid circular dependency issues // in the BaseItem class. @@ -27,10 +30,13 @@ BaseItem.loadClass('Folder', Folder); BaseItem.loadClass('Resource', Resource); BaseItem.loadClass('Tag', Tag); BaseItem.loadClass('NoteTag', NoteTag); +BaseItem.loadClass('MasterKey', MasterKey); Setting.setConstant('appId', 'net.cozic.joplin-desktop'); Setting.setConstant('appType', 'desktop'); +shimInit(); + // Disable drag and drop of links inside application (which would // open it as if the whole app was a browser) document.addEventListener('dragover', event => event.preventDefault()); @@ -44,8 +50,6 @@ document.addEventListener('auxclick', event => event.preventDefault()); // which would open a new browser window. document.addEventListener('click', (event) => event.preventDefault()); -shimInit(); - app().start(bridge().processArgv()).then(() => { require('./gui/Root.min.js'); }).catch((error) => { diff --git a/ElectronClient/app/package-lock.json b/ElectronClient/app/package-lock.json index c498289fa..eaf8c5901 100644 --- a/ElectronClient/app/package-lock.json +++ b/ElectronClient/app/package-lock.json @@ -1,6 +1,6 @@ { "name": "Joplin", - "version": "0.10.39", + "version": "0.10.41", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -10,13 +10,22 @@ "integrity": "sha512-+rr4OgeTNrLuJAf09o3USdttEYiXvZshWMkhD6wR9v1ieXH0JM1Q2yT41/cJuJcqiPpSXlM/g3aR+Y5MWQdr0Q==", "dev": true, "requires": { - "7zip-bin-mac": "1.0.1" + "7zip-bin-win": "2.1.1" + }, + "dependencies": { + "7zip-bin-win": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/7zip-bin-win/-/7zip-bin-win-2.1.1.tgz", + "integrity": "sha512-6VGEW7PXGroTsoI2QW3b0ea95HJmbVBHvfANKLLMzSzFA1zKqVX5ybNuhmeGpf6vA0x8FJTt6twpprDANsY5WQ==", + "dev": true, + "optional": true + } } }, - "7zip-bin-mac": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/7zip-bin-mac/-/7zip-bin-mac-1.0.1.tgz", - "integrity": "sha1-Pmh3i78JJq3GgVlCcHRQXUdVXAI=", + "7zip-bin-win": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/7zip-bin-win/-/7zip-bin-win-2.1.1.tgz", + "integrity": "sha512-6VGEW7PXGroTsoI2QW3b0ea95HJmbVBHvfANKLLMzSzFA1zKqVX5ybNuhmeGpf6vA0x8FJTt6twpprDANsY5WQ==", "optional": true }, "@types/node": { @@ -904,7 +913,6 @@ "requires": { "anymatch": "1.3.2", "async-each": "1.0.1", - "fsevents": "1.1.3", "glob-parent": "2.0.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -1142,6 +1150,11 @@ "array-find-index": "1.0.2" } }, + "currify": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/currify/-/currify-2.0.6.tgz", + "integrity": "sha512-F0lbcoBkA2FMcejFeHJkDEhQ1AvVkTpkn9PMzJch+7mHy5WdteZ9t+nhT6cOdga4uRay3rjvprgp8tUkixFy8w==" + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -1374,13 +1387,26 @@ "requires": { "debug": "3.1.0", "env-paths": "1.0.0", - "fs-extra": "4.0.2", + "fs-extra": "4.0.3", "minimist": "1.2.0", "nugget": "2.0.1", "path-exists": "3.0.0", "rc": "1.2.2", "semver": "5.4.1", "sumchecker": "2.0.2" + }, + "dependencies": { + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + } } }, "minimist": { @@ -1695,8 +1721,7 @@ "es6-promise": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz", - "integrity": "sha512-OaU1hHjgJf+b0NzsxCg7NdIYERD6Hy/PEmFLTjw+b65scuisG3Kt4QoTvJ66BBkPZ581gr0kpoVzKnxniM8nng==", - "dev": true + "integrity": "sha512-OaU1hHjgJf+b0NzsxCg7NdIYERD6Hy/PEmFLTjw+b65scuisG3Kt4QoTvJ66BBkPZ581gr0kpoVzKnxniM8nng==" }, "escape-string-regexp": { "version": "1.0.5", @@ -1947,9 +1972,9 @@ } }, "fs-extra": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.2.tgz", - "integrity": "sha1-+RcExT0bRh+JNFKwwwfZmXZHq2s=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", + "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", "requires": { "graceful-fs": "4.1.11", "jsonfile": "4.0.0", @@ -1962,7 +1987,19 @@ "integrity": "sha512-zHsMNJWhXD184QfHKEIFSQSgAFNV7v9J+Nt2XpaLZp2nTz6WxZNV+R4G2uYeGeLTMaKvUZiqGKrH/4iFCupcUA==", "requires": { "bluebird-lst": "1.0.5", - "fs-extra": "4.0.2" + "fs-extra": "4.0.3" + }, + "dependencies": { + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + } } }, "fs-readdir-recursive": { @@ -1977,909 +2014,10 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "fsevents": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", - "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.7.0", - "node-pre-gyp": "0.6.39" - }, - "dependencies": { - "abbrev": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "ajv": { - "version": "4.11.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.2.9" - } - }, - "asn1": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "assert-plus": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws-sign2": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws4": { - "version": "1.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "balanced-match": { - "version": "0.4.2", - "bundled": true, - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "block-stream": { - "version": "0.0.9", - "bundled": true, - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "boom": { - "version": "2.10.1", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "brace-expansion": { - "version": "1.1.7", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "0.4.2", - "concat-map": "0.0.1" - } - }, - "buffer-shims": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "caseless": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true - }, - "co": { - "version": "4.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "combined-stream": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "cryptiles": { - "version": "2.0.5", - "bundled": true, - "dev": true, - "requires": { - "boom": "2.10.1" - } - }, - "dashdash": { - "version": "1.14.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "debug": { - "version": "2.6.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "bundled": true, - "dev": true, - "optional": true - }, - "delayed-stream": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "extend": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "extsprintf": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "bundled": true, - "dev": true, - "optional": true - }, - "form-data": { - "version": "2.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.15" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "fstream": { - "version": "1.0.11", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.1" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.1.1", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "getpass": { - "version": "0.1.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true, - "dev": true - }, - "har-schema": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "har-validator": { - "version": "4.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "hawk": { - "version": "3.1.3", - "bundled": true, - "dev": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "bundled": true, - "dev": true - }, - "http-signature": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.0", - "sshpk": "1.13.0" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.4", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "isstream": { - "version": "0.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "jodid25519": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "jsonify": { - "version": "0.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "jsprim": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.0.2", - "json-schema": "0.2.3", - "verror": "1.3.6" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "mime-db": { - "version": "1.27.0", - "bundled": true, - "dev": true - }, - "mime-types": { - "version": "2.1.15", - "bundled": true, - "dev": true, - "requires": { - "mime-db": "1.27.0" - } - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "node-pre-gyp": { - "version": "0.6.39", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.2", - "hawk": "3.1.3", - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.0", - "rc": "1.2.1", - "request": "2.81.0", - "rimraf": "2.6.1", - "semver": "5.3.0", - "tar": "2.2.1", - "tar-pack": "3.4.0" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.0", - "osenv": "0.1.4" - } - }, - "npmlog": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "oauth-sign": { - "version": "0.8.2", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "performance-now": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "1.0.7", - "bundled": true, - "dev": true - }, - "punycode": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true - }, - "qs": { - "version": "6.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.4", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.2.9", - "bundled": true, - "dev": true, - "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "1.0.1", - "util-deprecate": "1.0.2" - } - }, - "request": { - "version": "2.81.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.15", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.0.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.2", - "tunnel-agent": "0.6.0", - "uuid": "3.0.1" - } - }, - "rimraf": { - "version": "2.6.1", - "bundled": true, - "dev": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, - "semver": { - "version": "5.3.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sntp": { - "version": "1.0.9", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "sshpk": { - "version": "1.13.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jodid25519": "1.0.2", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "stringstream": { - "version": "0.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "2.2.1", - "bundled": true, - "dev": true, - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.8", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.2.9", - "rimraf": "2.6.1", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "bundled": true, - "dev": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "uuid": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "verror": { - "version": "1.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "extsprintf": "1.0.2" - } - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - } - } + "fullstore": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fullstore/-/fullstore-1.1.0.tgz", + "integrity": "sha512-XNlCWr3KBIL97G8mTR+dZ/J648ECCffflfFRgZW3Zm7pO0PYnH/ZCbwZjV1Dw4LrrDdhV6gnayiIcmdIY4JTsw==" }, "gauge": { "version": "2.7.4", @@ -4862,6 +4000,16 @@ "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", "dev": true }, + "smalltalk": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/smalltalk/-/smalltalk-2.5.1.tgz", + "integrity": "sha512-LZYd80hd9DkXBUnm5AyMdMNx9XCxYZZskmrp3W6M77jhOEvzQ9SpDqtvDE7e8y1lfMeIAG9nIFU6Y5quZoXV5g==", + "requires": { + "currify": "2.0.6", + "es6-promise": "4.1.1", + "fullstore": "1.1.0" + } + }, "sntp": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", diff --git a/ElectronClient/app/package.json b/ElectronClient/app/package.json index 8bdb3345a..2cf42c7a3 100644 --- a/ElectronClient/app/package.json +++ b/ElectronClient/app/package.json @@ -1,6 +1,6 @@ { "name": "Joplin", - "version": "0.10.39", + "version": "0.10.41", "description": "Joplin for Desktop", "main": "main.js", "scripts": { @@ -57,7 +57,7 @@ "electron-window-state": "^4.1.1", "follow-redirects": "^1.2.5", "form-data": "^2.3.1", - "fs-extra": "^4.0.2", + "fs-extra": "^5.0.0", "highlight.js": "^9.12.0", "html-entities": "^1.2.1", "jssha": "^2.3.1", @@ -78,6 +78,7 @@ "react-redux": "^5.0.6", "redux": "^3.7.2", "sharp": "^0.18.4", + "smalltalk": "^2.5.1", "sprintf-js": "^1.1.1", "sqlite3": "^3.1.13", "string-padding": "^1.0.2", diff --git a/ElectronClient/app/theme.js b/ElectronClient/app/theme.js index 1fe56dc2c..ca27c8945 100644 --- a/ElectronClient/app/theme.js +++ b/ElectronClient/app/theme.js @@ -1,4 +1,4 @@ -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const globalStyle = { fontSize: 12, @@ -65,12 +65,16 @@ globalStyle.textStyle = { color: globalStyle.color, fontFamily: globalStyle.fontFamily, fontSize: globalStyle.fontSize, + lineHeight: '1.6em', }; globalStyle.textStyle2 = Object.assign({}, globalStyle.textStyle, { color: globalStyle.color2, }); +globalStyle.h1Style = Object.assign({}, globalStyle.textStyle); +globalStyle.h1Style.fontSize *= 1.5; + globalStyle.h2Style = Object.assign({}, globalStyle.textStyle); globalStyle.h2Style.fontSize *= 1.3; diff --git a/ElectronClient/build.sh b/ElectronClient/build.sh index e8b98e524..1abb7a66f 100755 --- a/ElectronClient/build.sh +++ b/ElectronClient/build.sh @@ -3,7 +3,7 @@ ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" BUILD_DIR="$ROOT_DIR/app" -rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/" +rsync -a --delete "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/" cd "$BUILD_DIR" npm run compile diff --git a/ElectronClient/release_version.sh b/ElectronClient/release_version.sh index f5d2e8816..bd650d7f3 100755 --- a/ElectronClient/release_version.sh +++ b/ElectronClient/release_version.sh @@ -14,4 +14,4 @@ echo "Create a draft release at: https://github.com/laurent22/joplin/releases/ta echo "" echo "Then run:" echo "" -echo "node $APP_DIR/update-readme-download.js && git add -A && git commit -m 'Update website' && git push" \ No newline at end of file +echo "git pull && node $APP_DIR/update-readme-download.js && git add -A && git commit -m 'Update website' && git push && git push --tags" \ No newline at end of file diff --git a/ElectronClient/run.sh b/ElectronClient/run.sh index eeebbf8dd..4e53825f1 100755 --- a/ElectronClient/run.sh +++ b/ElectronClient/run.sh @@ -3,4 +3,6 @@ ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd "$ROOT_DIR" ./build.sh || exit 1 cd "$ROOT_DIR/app" -./node_modules/.bin/electron . --env dev --log-level warn --open-dev-tools "$@" \ No newline at end of file +./node_modules/.bin/electron . --env dev --log-level debug --open-dev-tools "$@" + +#./node_modules/.bin/electron . --profile c:\\Users\\Laurent\\.config\\joplin-desktop --log-level debug --open-dev-tools "$@" \ No newline at end of file diff --git a/LICENSE b/LICENSE index 10b12bd5e..196fdb899 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017 Laurent Cozic +Copyright (c) 2016-2018 Laurent Cozic Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/LICENSE_fr b/LICENSE_fr index e2f89985e..d4dab1608 100644 --- a/LICENSE_fr +++ b/LICENSE_fr @@ -1,4 +1,4 @@ -Copyright (c) 2017 Laurent Cozic +Copyright (c) 2016-2018 Laurent Cozic L'autorisation est accordée, gracieusement, à toute personne acquérant une copie de ce logiciel et des fichiers de documentation associés (le "Logiciel"), de commercialiser le Logiciel sans restriction, notamment les droits d'utiliser, de copier, de modifier, de fusionner, de publier, de distribuer, de sous-licencier et/ou de vendre des copies du Logiciel, ainsi que d'autoriser les personnes auxquelles le Logiciel est fourni à le faire, sous réserve des conditions suivantes : diff --git a/README.md b/README.md index 4249bd806..92195e959 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ Three types of applications are available: for the **desktop** (Windows, macOS a Operating System | Download -----------------|-------- -Windows | Get it on Windows -macOS | Get it on macOS -Linux | Get it on macOS +Windows | Get it on Windows +macOS | Get it on macOS +Linux | Get it on macOS ## Mobile applications @@ -33,7 +33,7 @@ iOS | asList( new ImageResizerPackage(), new MainReactPackage(), + new RNSecureRandomPackage(), new ReactNativePushNotificationPackage(), new ImagePickerPackage(), new ReactNativeDocumentPicker(), diff --git a/ReactNativeClient/android/settings.gradle b/ReactNativeClient/android/settings.gradle index 3e108ed16..604b3a373 100644 --- a/ReactNativeClient/android/settings.gradle +++ b/ReactNativeClient/android/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'Joplin' +include ':react-native-securerandom' +project(':react-native-securerandom').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-securerandom/android') include ':react-native-push-notification' project(':react-native-push-notification').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-push-notification/android') include ':react-native-fs' diff --git a/ReactNativeClient/ios/Joplin.xcodeproj/project.pbxproj b/ReactNativeClient/ios/Joplin.xcodeproj/project.pbxproj index ea35f6340..82cc0998f 100644 --- a/ReactNativeClient/ios/Joplin.xcodeproj/project.pbxproj +++ b/ReactNativeClient/ios/Joplin.xcodeproj/project.pbxproj @@ -5,7 +5,6 @@ }; objectVersion = 46; objects = { - /* Begin PBXBuildFile section */ 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; @@ -43,6 +42,7 @@ EA501DCDCF4745E9B63ECE98 /* Octicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7D46CBDF8846409890AD7A84 /* Octicons.ttf */; }; EC11356C90E9419799A2626F /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 51BCEC3BC28046C8BB19531F /* EvilIcons.ttf */; }; FBF57CE2F0F448FA9A8985E2 /* libsqlite3.0.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0EB8BCAEA9AA41CAAE460443 /* libsqlite3.0.tbd */; }; + F3D0BB525E6C490294D73075 /* libRNSecureRandom.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 22647ACF9A4C45918C44C599 /* libRNSecureRandom.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -398,6 +398,8 @@ F5E37D05726A4A08B2EE323A /* libRNFetchBlob.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNFetchBlob.a; sourceTree = ""; }; FD370E24D76E461D960DD85D /* Feather.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Feather.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Feather.ttf"; sourceTree = ""; }; FF411B45E68B4A8CBCC35777 /* Ionicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Ionicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf"; sourceTree = ""; }; + 252BD7B86BF7435B960DA901 /* RNSecureRandom.xcodeproj */ = {isa = PBXFileReference; name = "RNSecureRandom.xcodeproj"; path = "../node_modules/react-native-securerandom/ios/RNSecureRandom.xcodeproj"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; }; + 22647ACF9A4C45918C44C599 /* libRNSecureRandom.a */ = {isa = PBXFileReference; name = "libRNSecureRandom.a"; path = "libRNSecureRandom.a"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -427,6 +429,7 @@ FBF57CE2F0F448FA9A8985E2 /* libsqlite3.0.tbd in Frameworks */, AE6BB3A2FDA34D01864A721A /* libRNVectorIcons.a in Frameworks */, AF99EEC6C55042F7BFC87583 /* libRNImagePicker.a in Frameworks */, + F3D0BB525E6C490294D73075 /* libRNSecureRandom.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -676,6 +679,7 @@ CCDE9E9AF09B45F391B1C2AF /* SQLite.xcodeproj */, 711CBD21F0894B83A2D8E234 /* RNVectorIcons.xcodeproj */, A4716DB8654B431D894F89E1 /* RNImagePicker.xcodeproj */, + 252BD7B86BF7435B960DA901 /* RNSecureRandom.xcodeproj */, ); name = Libraries; sourceTree = ""; @@ -1253,6 +1257,7 @@ "$(SRCROOT)/../node_modules/react-native-sqlite-storage/src/ios", "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", "$(SRCROOT)..\node_modules\neact-native-image-pickerios", + "$(SRCROOT)\..\node_modules\react-native-securerandom\ios", ); INFOPLIST_FILE = Joplin/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -1267,6 +1272,10 @@ PROVISIONING_PROFILE_SPECIFIER = ""; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/Joplin\"", + ); }; name = Debug; }; @@ -1288,6 +1297,7 @@ "$(SRCROOT)/../node_modules/react-native-sqlite-storage/src/ios", "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", "$(SRCROOT)..\node_modules\neact-native-image-pickerios", + "$(SRCROOT)\..\node_modules\react-native-securerandom\ios", ); INFOPLIST_FILE = Joplin/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -1302,6 +1312,10 @@ PROVISIONING_PROFILE_SPECIFIER = ""; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/Joplin\"", + ); }; name = Release; }; diff --git a/ReactNativeClient/lib/ArrayUtils.js b/ReactNativeClient/lib/ArrayUtils.js index 74e0797f8..06f5bd090 100644 --- a/ReactNativeClient/lib/ArrayUtils.js +++ b/ReactNativeClient/lib/ArrayUtils.js @@ -6,4 +6,11 @@ ArrayUtils.unique = function(array) { }); } +ArrayUtils.removeElement = function(array, element) { + const index = array.indexOf(element); + if (index < 0) return array; + array.splice(index, 1); + return array; +} + module.exports = ArrayUtils; \ No newline at end of file diff --git a/ReactNativeClient/lib/BaseApplication.js b/ReactNativeClient/lib/BaseApplication.js index 097313af1..bf4428795 100644 --- a/ReactNativeClient/lib/BaseApplication.js +++ b/ReactNativeClient/lib/BaseApplication.js @@ -4,12 +4,12 @@ const { JoplinDatabase } = require('lib/joplin-database.js'); const { Database } = require('lib/database.js'); const { FoldersScreenUtils } = require('lib/folders-screen-utils.js'); const { DatabaseDriverNode } = require('lib/database-driver-node.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Folder } = require('lib/models/folder.js'); -const { BaseItem } = require('lib/models/base-item.js'); -const { Note } = require('lib/models/note.js'); -const { Tag } = require('lib/models/tag.js'); -const { Setting } = require('lib/models/setting.js'); +const BaseModel = require('lib/BaseModel.js'); +const Folder = require('lib/models/Folder.js'); +const BaseItem = require('lib/models/BaseItem.js'); +const Note = require('lib/models/Note.js'); +const Tag = require('lib/models/Tag.js'); +const Setting = require('lib/models/Setting.js'); const { Logger } = require('lib/logger.js'); const { splitCommandString } = require('lib/string-utils.js'); const { sprintf } = require('sprintf-js'); @@ -26,6 +26,8 @@ const SyncTargetRegistry = require('lib/SyncTargetRegistry.js'); const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js'); const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js'); const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js'); +const EncryptionService = require('lib/services/EncryptionService'); +const DecryptionWorker = require('lib/services/DecryptionWorker'); SyncTargetRegistry.addClass(SyncTargetFilesystem); SyncTargetRegistry.addClass(SyncTargetOneDrive); @@ -265,6 +267,19 @@ class BaseApplication { time.setTimeFormat(Setting.value('timeFormat')); } + if ((action.type == 'SETTING_UPDATE_ONE' && (action.key.indexOf('encryption.') === 0)) || (action.type == 'SETTING_UPDATE_ALL')) { + if (this.hasGui()) { + await EncryptionService.instance().loadMasterKeysFromSettings(); + DecryptionWorker.instance().scheduleStart(); + const loadedMasterKeyIds = EncryptionService.instance().loadedMasterKeyIds(); + + this.dispatch({ + type: 'MASTERKEY_REMOVE_NOT_LOADED', + ids: loadedMasterKeyIds, + }); + } + } + if (action.type == 'TAG_SELECT' || action.type === 'TAG_DELETE') { await this.refreshNotes(newState); } @@ -289,6 +304,10 @@ class BaseApplication { reg.setupRecurrentSync(); } + if (this.hasGui() && action.type === 'SYNC_GOT_ENCRYPTED_ITEM') { + DecryptionWorker.instance().scheduleStart(); + } + return result; } @@ -306,6 +325,7 @@ class BaseApplication { FoldersScreenUtils.dispatch = this.store().dispatch; reg.dispatch = this.store().dispatch; BaseSyncTarget.dispatch = this.store().dispatch; + DecryptionWorker.instance().dispatch = this.store().dispatch; } async readFlagsFromFile(flagPath) { @@ -392,6 +412,12 @@ class BaseApplication { setLocale(Setting.value('locale')); } + EncryptionService.instance().setLogger(this.logger_); + BaseItem.encryptionService_ = EncryptionService.instance(); + DecryptionWorker.instance().setLogger(this.logger_); + DecryptionWorker.instance().setEncryptionService(EncryptionService.instance()); + await EncryptionService.instance().loadMasterKeysFromSettings(); + let currentFolderId = Setting.value('activeFolderId'); let currentFolder = null; if (currentFolderId) currentFolder = await Folder.load(currentFolderId); diff --git a/ReactNativeClient/lib/base-model.js b/ReactNativeClient/lib/BaseModel.js similarity index 89% rename from ReactNativeClient/lib/base-model.js rename to ReactNativeClient/lib/BaseModel.js index d4a9a0cbf..9a1386a87 100644 --- a/ReactNativeClient/lib/base-model.js +++ b/ReactNativeClient/lib/BaseModel.js @@ -96,8 +96,11 @@ class BaseModel { return options; } - static count() { - return this.db().selectOne('SELECT count(*) as total FROM `' + this.tableName() + '`').then((r) => { + static count(options = null) { + if (!options) options = {}; + let sql = 'SELECT count(*) as total FROM `' + this.tableName() + '`'; + if (options.where) sql += ' WHERE ' + options.where; + return this.db().selectOne(sql).then((r) => { return r ? r['total'] : 0; }); } @@ -200,18 +203,37 @@ class BaseModel { static diffObjects(oldModel, newModel) { let output = {}; - let type = null; + const fields = this.diffObjectsFields(oldModel, newModel); + for (let i = 0; i < fields.length; i++) { + output[fields[i]] = newModel[fields[i]]; + } + if ('type_' in newModel) output.type_ = newModel.type_; + return output; + // let output = {}; + // let type = null; + // for (let n in newModel) { + // if (!newModel.hasOwnProperty(n)) continue; + // if (n == 'type_') { + // type = newModel[n]; + // continue; + // } + // if (!(n in oldModel) || newModel[n] !== oldModel[n]) { + // output[n] = newModel[n]; + // } + // } + // if (type !== null) output.type_ = type; + // return output; + } + + static diffObjectsFields(oldModel, newModel) { + let output = []; for (let n in newModel) { if (!newModel.hasOwnProperty(n)) continue; - if (n == 'type_') { - type = newModel[n]; - continue; - } + if (n == 'type_') continue; if (!(n in oldModel) || newModel[n] !== oldModel[n]) { - output[n] = newModel[n]; + output.push(n); } } - if (type !== null) output.type_ = type; return output; } @@ -269,6 +291,16 @@ class BaseModel { let where = { id: o.id }; let temp = Object.assign({}, o); delete temp.id; + + if (options.fields) { + let filtered = {}; + for (let i = 0; i < options.fields.length; i++) { + const f = options.fields[i]; + filtered[f] = o[f]; + } + temp = filtered; + } + query = Database.updateQuery(this.tableName(), temp, where); } @@ -401,8 +433,9 @@ BaseModel.TYPE_TAG = 5; BaseModel.TYPE_NOTE_TAG = 6; BaseModel.TYPE_SEARCH = 7; BaseModel.TYPE_ALARM = 8; +BaseModel.TYPE_MASTER_KEY = 9; BaseModel.db_ = null; BaseModel.dispatch = function(o) {}; -module.exports = { BaseModel }; \ No newline at end of file +module.exports = BaseModel; \ No newline at end of file diff --git a/ReactNativeClient/lib/BaseSyncTarget.js b/ReactNativeClient/lib/BaseSyncTarget.js index bdd76bb7c..b72d1d8f5 100644 --- a/ReactNativeClient/lib/BaseSyncTarget.js +++ b/ReactNativeClient/lib/BaseSyncTarget.js @@ -1,4 +1,4 @@ -const { reg } = require('lib/registry.js'); +const EncryptionService = require('lib/services/EncryptionService.js'); class BaseSyncTarget { @@ -88,6 +88,7 @@ class BaseSyncTarget { try { this.synchronizer_ = await this.initSynchronizer(); this.synchronizer_.setLogger(this.logger()); + this.synchronizer_.setEncryptionService(EncryptionService.instance()); this.synchronizer_.dispatch = BaseSyncTarget.dispatch; this.initState_ = 'ready'; return this.synchronizer_; diff --git a/ReactNativeClient/lib/JoplinError.js b/ReactNativeClient/lib/JoplinError.js new file mode 100644 index 000000000..e4e227382 --- /dev/null +++ b/ReactNativeClient/lib/JoplinError.js @@ -0,0 +1,14 @@ +class JoplinError extends Error { + + constructor(message, code = null) { + super(message); + this.code_ = code; + } + + get code() { + return this.code_; + } + +} + +module.exports = JoplinError; \ No newline at end of file diff --git a/ReactNativeClient/lib/MdToHtml.js b/ReactNativeClient/lib/MdToHtml.js index c181d5612..24558e346 100644 --- a/ReactNativeClient/lib/MdToHtml.js +++ b/ReactNativeClient/lib/MdToHtml.js @@ -1,7 +1,7 @@ const MarkdownIt = require('markdown-it'); const Entities = require('html-entities').AllHtmlEntities; const htmlentities = (new Entities()).encode; -const { Resource } = require('lib/models/resource.js'); +const Resource = require('lib/models/Resource.js'); const ModelCache = require('lib/ModelCache'); const { shim } = require('lib/shim.js'); const md5 = require('md5'); diff --git a/ReactNativeClient/lib/SyncTargetFilesystem.js b/ReactNativeClient/lib/SyncTargetFilesystem.js index 31e6254c3..cf472a55e 100644 --- a/ReactNativeClient/lib/SyncTargetFilesystem.js +++ b/ReactNativeClient/lib/SyncTargetFilesystem.js @@ -1,6 +1,6 @@ const BaseSyncTarget = require('lib/BaseSyncTarget.js'); const { _ } = require('lib/locale.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { FileApi } = require('lib/file-api.js'); const { FileApiDriverLocal } = require('lib/file-api-driver-local.js'); const { Synchronizer } = require('lib/synchronizer.js'); diff --git a/ReactNativeClient/lib/SyncTargetMemory.js b/ReactNativeClient/lib/SyncTargetMemory.js index d47fedfa5..d0ac33461 100644 --- a/ReactNativeClient/lib/SyncTargetMemory.js +++ b/ReactNativeClient/lib/SyncTargetMemory.js @@ -1,6 +1,6 @@ const BaseSyncTarget = require('lib/BaseSyncTarget.js'); const { _ } = require('lib/locale.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { FileApi } = require('lib/file-api.js'); const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js'); const { Synchronizer } = require('lib/synchronizer.js'); diff --git a/ReactNativeClient/lib/SyncTargetOneDrive.js b/ReactNativeClient/lib/SyncTargetOneDrive.js index be71041bb..03d0c62dc 100644 --- a/ReactNativeClient/lib/SyncTargetOneDrive.js +++ b/ReactNativeClient/lib/SyncTargetOneDrive.js @@ -1,7 +1,7 @@ const BaseSyncTarget = require('lib/BaseSyncTarget.js'); const { _ } = require('lib/locale.js'); const { OneDriveApi } = require('lib/onedrive-api.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { parameters } = require('lib/parameters.js'); const { FileApi } = require('lib/file-api.js'); const { Synchronizer } = require('lib/synchronizer.js'); diff --git a/ReactNativeClient/lib/SyncTargetOneDriveDev.js b/ReactNativeClient/lib/SyncTargetOneDriveDev.js index aa3ed7231..f60f430d3 100644 --- a/ReactNativeClient/lib/SyncTargetOneDriveDev.js +++ b/ReactNativeClient/lib/SyncTargetOneDriveDev.js @@ -2,7 +2,7 @@ const BaseSyncTarget = require('lib/BaseSyncTarget.js'); const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js'); const { _ } = require('lib/locale.js'); const { OneDriveApi } = require('lib/onedrive-api.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { parameters } = require('lib/parameters.js'); const { FileApi } = require('lib/file-api.js'); const { Synchronizer } = require('lib/synchronizer.js'); diff --git a/ReactNativeClient/lib/components/global-style.js b/ReactNativeClient/lib/components/global-style.js index 8eeebb594..874cb78bc 100644 --- a/ReactNativeClient/lib/components/global-style.js +++ b/ReactNativeClient/lib/components/global-style.js @@ -1,4 +1,4 @@ -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const globalStyle = { fontSize: 16, @@ -19,6 +19,8 @@ const globalStyle = { raisedColor: "#003363", raisedHighlightedColor: "#ffffff", + warningBackgroundColor: "#FFD08D", + // For WebView - must correspond to the properties above htmlFontSize: '16px', htmlColor: 'black', // Note: CSS in WebView component only supports named colors or rgb() notation diff --git a/ReactNativeClient/lib/components/note-body-viewer.js b/ReactNativeClient/lib/components/note-body-viewer.js index d305fe6f1..b4f025261 100644 --- a/ReactNativeClient/lib/components/note-body-viewer.js +++ b/ReactNativeClient/lib/components/note-body-viewer.js @@ -1,8 +1,8 @@ const React = require('react'); const Component = React.Component; const { Platform, WebView, View, Linking } = require('react-native'); const { globalStyle } = require('lib/components/global-style.js'); -const { Resource } = require('lib/models/resource.js'); -const { Setting } = require('lib/models/setting.js'); +const Resource = require('lib/models/Resource.js'); +const Setting = require('lib/models/Setting.js'); const { reg } = require('lib/registry.js'); const MdToHtml = require('lib/MdToHtml.js'); diff --git a/ReactNativeClient/lib/components/note-item.js b/ReactNativeClient/lib/components/note-item.js index 506fb35f4..f000eed12 100644 --- a/ReactNativeClient/lib/components/note-item.js +++ b/ReactNativeClient/lib/components/note-item.js @@ -5,7 +5,7 @@ const { Log } = require('lib/log.js'); const { _ } = require('lib/locale.js'); const { Checkbox } = require('lib/components/checkbox.js'); const { reg } = require('lib/registry.js'); -const { Note } = require('lib/models/note.js'); +const Note = require('lib/models/Note.js'); const { time } = require('lib/time-utils.js'); const { globalStyle, themeStyle } = require('lib/components/global-style.js'); @@ -81,6 +81,7 @@ class NoteItemComponent extends Component { onPress() { if (!this.props.note) return; + if (!!this.props.note.encryption_applied) return; if (this.props.noteSelectionEnabled) { this.props.dispatch({ @@ -141,7 +142,7 @@ class NoteItemComponent extends Component { checked={checkboxChecked} onChange={(checked) => this.todoCheckbox_change(checked)} /> - {note.title} + {Note.displayTitle(note)} diff --git a/ReactNativeClient/lib/components/note-list.js b/ReactNativeClient/lib/components/note-list.js index a8eec9ed2..1f668e9f3 100644 --- a/ReactNativeClient/lib/components/note-list.js +++ b/ReactNativeClient/lib/components/note-list.js @@ -6,8 +6,8 @@ const { _ } = require('lib/locale.js'); const { Checkbox } = require('lib/components/checkbox.js'); const { NoteItem } = require('lib/components/note-item.js'); const { reg } = require('lib/registry.js'); -const { Note } = require('lib/models/note.js'); -const { Setting } = require('lib/models/setting.js'); +const Note = require('lib/models/Note.js'); +const Setting = require('lib/models/Setting.js'); const { time } = require('lib/time-utils.js'); const { themeStyle } = require('lib/components/global-style.js'); diff --git a/ReactNativeClient/lib/components/screen-header.js b/ReactNativeClient/lib/components/screen-header.js index f746b376b..f6fad349e 100644 --- a/ReactNativeClient/lib/components/screen-header.js +++ b/ReactNativeClient/lib/components/screen-header.js @@ -7,9 +7,9 @@ const { BackButtonService } = require('lib/services/back-button.js'); const { ReportService } = require('lib/services/report.js'); const { Menu, MenuOptions, MenuOption, MenuTrigger } = require('react-native-popup-menu'); const { _ } = require('lib/locale.js'); -const { Setting } = require('lib/models/setting.js'); -const { Note } = require('lib/models/note.js'); -const { Folder } = require('lib/models/folder.js'); +const Setting = require('lib/models/Setting.js'); +const Note = require('lib/models/Note.js'); +const Folder = require('lib/models/Folder.js'); const { FileApi } = require('lib/file-api.js'); const { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js'); const { reg } = require('lib/registry.js'); @@ -43,7 +43,7 @@ class ScreenHeaderComponent extends Component { let styleObject = { container: { - flexDirection: 'row', + flexDirection: 'column', backgroundColor: theme.raisedBackgroundColor, alignItems: 'center', shadowColor: '#000000', @@ -123,11 +123,17 @@ class ScreenHeaderComponent extends Component { }, titleText: { flex: 1, + textAlignVertical: 'center', marginLeft: 0, color: theme.raisedHighlightedColor, fontWeight: 'bold', fontSize: theme.fontSize, - } + }, + warningBox: { + backgroundColor: "#ff9900", + flexDirection: 'row', + padding: theme.marginLeft, + }, }; styleObject.topIcon = Object.assign({}, theme.icon); @@ -198,6 +204,20 @@ class ScreenHeaderComponent extends Component { }); } + encryptionConfig_press() { + this.props.dispatch({ + type: 'NAV_GO', + routeName: 'EncryptionConfig', + }); + } + + warningBox_press() { + this.props.dispatch({ + type: 'NAV_GO', + routeName: 'EncryptionConfig', + }); + } + async debugReport_press() { const service = new ReportService(); @@ -324,6 +344,11 @@ class ScreenHeaderComponent extends Component { menuOptionComponents.push(); } + menuOptionComponents.push( + this.encryptionConfig_press()} key={'menuOption_encryptionConfig'} style={this.styles().contextMenuItem}> + {_('Encryption Config')} + ); + menuOptionComponents.push( this.config_press()} key={'menuOption_config'} style={this.styles().contextMenuItem}> {_('Configuration')} @@ -405,6 +430,12 @@ class ScreenHeaderComponent extends Component { } } + const warningComp = this.props.showMissingMasterKeyMessage ? ( + this.warningBox_press()} activeOpacity={0.8}> + {_('Press to set the decryption password.')} + + ) : null; + const titleComp = createTitleComponent(); const sideMenuComp = this.props.noteSelectionEnabled ? null : sideMenuButton(this.styles(), () => this.sideMenuButton_press()); const backButtonComp = backButton(this.styles(), () => this.backButton_press(), !this.props.historyCanGoBack); @@ -427,13 +458,16 @@ class ScreenHeaderComponent extends Component { return ( - { sideMenuComp } - { backButtonComp } - { saveButton(this.styles(), () => { if (this.props.onSaveButtonPress) this.props.onSaveButtonPress() }, this.props.saveButtonDisabled === true, this.props.showSaveButton === true) } - { titleComp } - { searchButtonComp } - { deleteButtonComp } - { menuComp } + + { sideMenuComp } + { backButtonComp } + { saveButton(this.styles(), () => { if (this.props.onSaveButtonPress) this.props.onSaveButtonPress() }, this.props.saveButtonDisabled === true, this.props.showSaveButton === true) } + { titleComp } + { searchButtonComp } + { deleteButtonComp } + { menuComp } + + { warningComp } { this.dialogbox = dialogbox }}/> ); @@ -455,6 +489,7 @@ const ScreenHeader = connect( showAdvancedOptions: state.settings.showAdvancedOptions, noteSelectionEnabled: state.noteSelectionEnabled, selectedNoteIds: state.selectedNoteIds, + showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length, }; } )(ScreenHeaderComponent) diff --git a/ReactNativeClient/lib/components/screens/config.js b/ReactNativeClient/lib/components/screens/config.js index bdaf9c2cf..b685b8f5f 100644 --- a/ReactNativeClient/lib/components/screens/config.js +++ b/ReactNativeClient/lib/components/screens/config.js @@ -6,7 +6,7 @@ const { _, setLocale } = require('lib/locale.js'); const { BaseScreenComponent } = require('lib/components/base-screen.js'); const { Dropdown } = require('lib/components/Dropdown.js'); const { themeStyle } = require('lib/components/global-style.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); class ConfigScreenComponent extends BaseScreenComponent { diff --git a/ReactNativeClient/lib/components/screens/encryption-config.js b/ReactNativeClient/lib/components/screens/encryption-config.js new file mode 100644 index 000000000..064c0aaa1 --- /dev/null +++ b/ReactNativeClient/lib/components/screens/encryption-config.js @@ -0,0 +1,228 @@ +const React = require('react'); const Component = React.Component; +const { TextInput, TouchableOpacity, Linking, View, Switch, Slider, StyleSheet, Text, Button, ScrollView } = require('react-native'); +const EncryptionService = require('lib/services/EncryptionService'); +const { connect } = require('react-redux'); +const { ScreenHeader } = require('lib/components/screen-header.js'); +const { _ } = require('lib/locale.js'); +const { BaseScreenComponent } = require('lib/components/base-screen.js'); +const { Dropdown } = require('lib/components/Dropdown.js'); +const { themeStyle } = require('lib/components/global-style.js'); +const { time } = require('lib/time-utils.js'); +const Setting = require('lib/models/Setting.js'); +const shared = require('lib/components/shared/encryption-config-shared.js'); +const { dialogs } = require('lib/dialogs.js'); +const DialogBox = require('react-native-dialogbox').default; + +class EncryptionConfigScreenComponent extends BaseScreenComponent { + + static navigationOptions(options) { + return { header: null }; + } + + constructor() { + super(); + + this.state = { + passwordPromptShow: false, + passwordPromptAnswer: '', + }; + + shared.constructor(this); + + this.styles_ = {}; + } + + componentDidMount() { + this.isMounted_ = true; + } + + componentWillUnmount() { + this.isMounted_ = false; + } + + initState(props) { + return shared.initState(this, props); + } + + async refreshStats() { + return shared.refreshStats(this); + } + + componentWillMount() { + this.initState(this.props); + } + + componentWillReceiveProps(nextProps) { + this.initState(nextProps); + } + + async checkPasswords() { + return shared.checkPasswords(this); + } + + styles() { + const themeId = this.props.theme; + const theme = themeStyle(themeId); + + if (this.styles_[themeId]) return this.styles_[themeId]; + this.styles_ = {}; + + let styles = { + titleText: { + flex: 1, + fontWeight: 'bold', + flexDirection: 'column', + fontSize: theme.fontSize, + paddingTop: 5, + paddingBottom: 5, + marginTop: theme.marginTop, + marginBottom: 5, + color: theme.color, + }, + normalText: { + flex: 1, + fontSize: theme.fontSize, + color: theme.color, + }, + container: { + flex: 1, + padding: theme.margin, + }, + } + + this.styles_[themeId] = StyleSheet.create(styles); + return this.styles_[themeId]; + } + + renderMasterKey(num, mk) { + const theme = themeStyle(this.props.theme); + + const onSaveClick = () => { + return shared.onSavePasswordClick(this, mk); + } + + const onPasswordChange = (text) => { + return shared.onPasswordChange(this, mk, text); + } + + const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : ''; + const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌'; + const active = this.props.activeMasterKeyId === mk.id ? '✔' : ''; + + return ( + + {_('Master Key %s', mk.id.substr(0,6))} + {_('Created: %s', time.formatMsToLocal(mk.created_time))} + + {_('Password:')} + onPasswordChange(text)} style={{flex:1, marginRight: 10, color: theme.color}}> + {passwordOk} + + + + ); + } + + passwordPromptComponent() { + const theme = themeStyle(this.props.theme); + + const onEnableClick = async () => { + try { + const password = this.state.passwordPromptAnswer; + if (!password) throw new Error(_('Password cannot be empty')); + await EncryptionService.instance().generateMasterKeyAndEnableEncryption(password); + this.setState({ passwordPromptShow: false }); + } catch (error) { + await dialogs.error(this, error.message); + } + } + + return ( + + {_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')} + { this.setState({ passwordPromptAnswer: text }) }}> + + + + + + + + + + ); + } + + render() { + const theme = themeStyle(this.props.theme); + const masterKeys = this.state.masterKeys; + const decryptedItemsInfo = this.props.encryptionEnabled ? {shared.decryptedStatText(this)} : null; + + const mkComps = []; + for (let i = 0; i < masterKeys.length; i++) { + const mk = masterKeys[i]; + mkComps.push(this.renderMasterKey(i+1, mk)); + } + + const onToggleButtonClick = async () => { + if (this.props.encryptionEnabled) { + const ok = await dialogs.confirm(this, _('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?')); + if (!ok) return; + + try { + await EncryptionService.instance().disableEncryption(); + } catch (error) { + await dialogs.error(this, error.message); + } + } else { + this.setState({ + passwordPromptShow: true, + passwordPromptAnswer: '', + }); + return; + } + }; + + const passwordPromptComp = this.state.passwordPromptShow ? this.passwordPromptComponent() : null; + const toggleButton = !this.state.passwordPromptShow ? : null; + + return ( + + + + + + Important: This is a *beta* feature. It has been extensively tested and is already in use by some users, but it is possible that some bugs remain. + If you wish to you use it, it is recommended that you keep a backup of your data. The simplest way is to regularly backup your notes from the desktop or terminal application. + For more information about End-To-End Encryption (E2EE) and how it is going to work, please check the documentation: + { Linking.openURL('http://joplin.cozic.net/help/e2ee.html') }}>http://joplin.cozic.net/help/e2ee.html + + + {_('Status')} + {_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))} + {decryptedItemsInfo} + {toggleButton} + {passwordPromptComp} + {mkComps} + + + { this.dialogbox = dialogbox }}/> + + ); + } + +} + +const EncryptionConfigScreen = connect( + (state) => { + return { + theme: state.settings.theme, + masterKeys: state.masterKeys, + passwords: state.settings['encryption.passwordCache'], + encryptionEnabled: state.settings['encryption.enabled'], + activeMasterKeyId: state.settings['encryption.activeMasterKeyId'], + }; + } +)(EncryptionConfigScreenComponent) + +module.exports = { EncryptionConfigScreen }; \ No newline at end of file diff --git a/ReactNativeClient/lib/components/screens/folder.js b/ReactNativeClient/lib/components/screens/folder.js index c9c779786..288f4f4a9 100644 --- a/ReactNativeClient/lib/components/screens/folder.js +++ b/ReactNativeClient/lib/components/screens/folder.js @@ -3,8 +3,8 @@ const { View, Button, TextInput, StyleSheet } = require('react-native'); const { connect } = require('react-redux'); const { Log } = require('lib/log.js'); const { ActionButton } = require('lib/components/action-button.js'); -const { Folder } = require('lib/models/folder.js'); -const { BaseModel } = require('lib/base-model.js'); +const Folder = require('lib/models/Folder.js'); +const BaseModel = require('lib/BaseModel.js'); const { ScreenHeader } = require('lib/components/screen-header.js'); const { reg } = require('lib/registry.js'); const { BaseScreenComponent } = require('lib/components/base-screen.js'); diff --git a/ReactNativeClient/lib/components/screens/note.js b/ReactNativeClient/lib/components/screens/note.js index 8e7eb42dd..5c1be73de 100644 --- a/ReactNativeClient/lib/components/screens/note.js +++ b/ReactNativeClient/lib/components/screens/note.js @@ -4,12 +4,12 @@ const { connect } = require('react-redux'); const { uuid } = require('lib/uuid.js'); const { Log } = require('lib/log.js'); const RNFS = require('react-native-fs'); -const { Note } = require('lib/models/note.js'); -const { Setting } = require('lib/models/setting.js'); -const { Resource } = require('lib/models/resource.js'); -const { Folder } = require('lib/models/folder.js'); +const Note = require('lib/models/Note.js'); +const Setting = require('lib/models/Setting.js'); +const Resource = require('lib/models/Resource.js'); +const Folder = require('lib/models/Folder.js'); const { BackButtonService } = require('lib/services/back-button.js'); -const { BaseModel } = require('lib/base-model.js'); +const BaseModel = require('lib/BaseModel.js'); const { ActionButton } = require('lib/components/action-button.js'); const Icon = require('react-native-vector-icons/Ionicons').default; const { fileExtension, basename, safeFileExtension } = require('lib/path-utils.js'); diff --git a/ReactNativeClient/lib/components/screens/notes.js b/ReactNativeClient/lib/components/screens/notes.js index 9581f3013..38234fd84 100644 --- a/ReactNativeClient/lib/components/screens/notes.js +++ b/ReactNativeClient/lib/components/screens/notes.js @@ -4,10 +4,10 @@ const { connect } = require('react-redux'); const { reg } = require('lib/registry.js'); const { Log } = require('lib/log.js'); const { NoteList } = require('lib/components/note-list.js'); -const { Folder } = require('lib/models/folder.js'); -const { Tag } = require('lib/models/tag.js'); -const { Note } = require('lib/models/note.js'); -const { Setting } = require('lib/models/setting.js'); +const Folder = require('lib/models/Folder.js'); +const Tag = require('lib/models/Tag.js'); +const Note = require('lib/models/Note.js'); +const Setting = require('lib/models/Setting.js'); const { themeStyle } = require('lib/components/global-style.js'); const { ScreenHeader } = require('lib/components/screen-header.js'); const { MenuOption, Text } = require('react-native-popup-menu'); @@ -95,10 +95,13 @@ class NotesScreenComponent extends BaseScreenComponent { if (this.props.notesParentType == 'Folder') { if (this.props.selectedFolderId == Folder.conflictFolderId()) return []; - return [ - { title: _('Delete notebook'), onPress: () => { this.deleteFolder_onPress(this.props.selectedFolderId); } }, - { title: _('Edit notebook'), onPress: () => { this.editFolder_onPress(this.props.selectedFolderId); } }, - ]; + const folder = this.parentItem(); + + let output = []; + if (!folder.encryption_applied) output.push({ title: _('Edit notebook'), onPress: () => { this.editFolder_onPress(this.props.selectedFolderId); } }); + output.push({ title: _('Delete notebook'), onPress: () => { this.deleteFolder_onPress(this.props.selectedFolderId); } }); + + return output; } else { return []; // For tags - TODO } diff --git a/ReactNativeClient/lib/components/screens/onedrive-login.js b/ReactNativeClient/lib/components/screens/onedrive-login.js index 5706a40be..65e525d34 100644 --- a/ReactNativeClient/lib/components/screens/onedrive-login.js +++ b/ReactNativeClient/lib/components/screens/onedrive-login.js @@ -3,7 +3,7 @@ const { View } = require('react-native'); const { WebView, Button, Text } = require('react-native'); const { connect } = require('react-redux'); const { Log } = require('lib/log.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { ScreenHeader } = require('lib/components/screen-header.js'); const { reg } = require('lib/registry.js'); const { _ } = require('lib/locale.js'); diff --git a/ReactNativeClient/lib/components/screens/search.js b/ReactNativeClient/lib/components/screens/search.js index 3ef5b6096..6ace75715 100644 --- a/ReactNativeClient/lib/components/screens/search.js +++ b/ReactNativeClient/lib/components/screens/search.js @@ -4,7 +4,7 @@ const { connect } = require('react-redux'); const { ScreenHeader } = require('lib/components/screen-header.js'); const Icon = require('react-native-vector-icons/Ionicons').default; const { _ } = require('lib/locale.js'); -const { Note } = require('lib/models/note.js'); +const Note = require('lib/models/Note.js'); const { NoteItem } = require('lib/components/note-item.js'); const { BaseScreenComponent } = require('lib/components/base-screen.js'); const { themeStyle } = require('lib/components/global-style.js'); diff --git a/ReactNativeClient/lib/components/screens/status.js b/ReactNativeClient/lib/components/screens/status.js index abb3cd9d7..8ce07e0b9 100644 --- a/ReactNativeClient/lib/components/screens/status.js +++ b/ReactNativeClient/lib/components/screens/status.js @@ -1,15 +1,15 @@ const React = require('react'); const Component = React.Component; const { ListView, StyleSheet, View, Text, Button, FlatList } = require('react-native'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { connect } = require('react-redux'); const { Log } = require('lib/log.js'); const { reg } = require('lib/registry.js'); const { ScreenHeader } = require('lib/components/screen-header.js'); const { time } = require('lib/time-utils'); const { Logger } = require('lib/logger.js'); -const { BaseItem } = require('lib/models/base-item.js'); +const BaseItem = require('lib/models/BaseItem.js'); const { Database } = require('lib/database.js'); -const { Folder } = require('lib/models/folder.js'); +const Folder = require('lib/models/Folder.js'); const { ReportService } = require('lib/services/report.js'); const { _ } = require('lib/locale.js'); const { BaseScreenComponent } = require('lib/components/base-screen.js'); diff --git a/ReactNativeClient/lib/components/screens/tag.js b/ReactNativeClient/lib/components/screens/tag.js index 150672f1e..df3bdba2f 100644 --- a/ReactNativeClient/lib/components/screens/tag.js +++ b/ReactNativeClient/lib/components/screens/tag.js @@ -4,7 +4,7 @@ const { connect } = require('react-redux'); const { ScreenHeader } = require('lib/components/screen-header.js'); const Icon = require('react-native-vector-icons/Ionicons').default; const { _ } = require('lib/locale.js'); -const { Note } = require('lib/models/note.js'); +const Note = require('lib/models/Note.js'); const { NoteItem } = require('lib/components/note-item.js'); const { BaseScreenComponent } = require('lib/components/base-screen.js'); const { globalStyle } = require('lib/components/global-style.js'); diff --git a/ReactNativeClient/lib/components/shared/encryption-config-shared.js b/ReactNativeClient/lib/components/shared/encryption-config-shared.js new file mode 100644 index 000000000..ad57ddc6a --- /dev/null +++ b/ReactNativeClient/lib/components/shared/encryption-config-shared.js @@ -0,0 +1,86 @@ +const EncryptionService = require('lib/services/EncryptionService'); +const { _ } = require('lib/locale.js'); +const BaseItem = require('lib/models/BaseItem.js'); +const Setting = require('lib/models/Setting.js'); + +const shared = {}; + +shared.constructor = function(comp) { + comp.state = { + masterKeys: [], + passwords: {}, + passwordChecks: {}, + stats: { + encrypted: null, + total: null, + }, + }; + comp.isMounted_ = false; + + comp.refreshStatsIID_ = null; +} + +shared.initState = function(comp, props) { + comp.setState({ + masterKeys: props.masterKeys, + passwords: props.passwords ? props.passwords : {}, + }, () => { + comp.checkPasswords(); + }); + + comp.refreshStats(); + + if (comp.refreshStatsIID_) { + clearInterval(comp.refreshStatsIID_); + comp.refreshStatsIID_ = null; + } + + comp.refreshStatsIID_ = setInterval(() => { + if (!comp.isMounted_) { + clearInterval(comp.refreshStatsIID_); + comp.refreshStatsIID_ = null; + return; + } + comp.refreshStats(); + }, 3000); +} + +shared.refreshStats = async function(comp) { + const stats = await BaseItem.encryptedItemsStats(); + comp.setState({ stats: stats }); +} + +shared.checkPasswords = async function(comp) { + const passwordChecks = Object.assign({}, comp.state.passwordChecks); + for (let i = 0; i < comp.state.masterKeys.length; i++) { + const mk = comp.state.masterKeys[i]; + const password = comp.state.passwords[mk.id]; + const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false; + passwordChecks[mk.id] = ok; + } + comp.setState({ passwordChecks: passwordChecks }); +} + +shared.decryptedStatText = function(comp) { + const stats = comp.state.stats; + return _('Decrypted items: %s / %s', stats.encrypted !== null ? (stats.total - stats.encrypted) : '-', stats.total !== null ? stats.total : '-'); +} + +shared.onSavePasswordClick = function(comp, mk) { + const password = comp.state.passwords[mk.id]; + if (!password) { + Setting.deleteObjectKey('encryption.passwordCache', mk.id); + } else { + Setting.setObjectKey('encryption.passwordCache', mk.id, password); + } + + comp.checkPasswords(); +} + +shared.onPasswordChange = function(comp, mk, password) { + const passwords = comp.state.passwords; + passwords[mk.id] = password; + comp.setState({ passwords: passwords }); +} + +module.exports = shared; \ No newline at end of file diff --git a/ReactNativeClient/lib/components/shared/note-screen-shared.js b/ReactNativeClient/lib/components/shared/note-screen-shared.js index 3e1328f4f..5d59dbb47 100644 --- a/ReactNativeClient/lib/components/shared/note-screen-shared.js +++ b/ReactNativeClient/lib/components/shared/note-screen-shared.js @@ -1,7 +1,7 @@ const { reg } = require('lib/registry.js'); -const { Folder } = require('lib/models/folder.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Note } = require('lib/models/note.js'); +const Folder = require('lib/models/Folder.js'); +const BaseModel = require('lib/BaseModel.js'); +const Note = require('lib/models/Note.js'); const shared = {}; @@ -37,16 +37,23 @@ shared.saveNoteButton_press = async function(comp) { } // Save only the properties that have changed - let diff = null; + // let diff = null; + // if (!isNew) { + // diff = BaseModel.diffObjects(comp.state.lastSavedNote, note); + // diff.type_ = note.type_; + // diff.id = note.id; + // } else { + // diff = Object.assign({}, note); + // } + + // const savedNote = await Note.save(diff); + + let options = {}; if (!isNew) { - diff = BaseModel.diffObjects(comp.state.lastSavedNote, note); - diff.type_ = note.type_; - diff.id = note.id; - } else { - diff = Object.assign({}, note); + options.fields = BaseModel.diffObjectsFields(comp.state.lastSavedNote, note); } - const savedNote = await Note.save(diff); + const savedNote = ('fields' in options) && !options.fields.length ? Object.assign({}, note) : await Note.save(note, { userSideValidation: true }); const stateNote = comp.state.note; // Re-assign any property that might have changed during saving (updated_time, etc.) diff --git a/ReactNativeClient/lib/components/shared/side-menu-shared.js b/ReactNativeClient/lib/components/shared/side-menu-shared.js index 3accd3533..8cedc59c0 100644 --- a/ReactNativeClient/lib/components/shared/side-menu-shared.js +++ b/ReactNativeClient/lib/components/shared/side-menu-shared.js @@ -31,7 +31,7 @@ shared.renderSearches = function(props, renderItem) { } shared.synchronize_press = async function(comp) { - const { Setting } = require('lib/models/setting.js'); + const Setting = require('lib/models/Setting.js'); const { reg } = require('lib/registry.js'); const action = comp.props.syncStarted ? 'cancel' : 'start'; diff --git a/ReactNativeClient/lib/components/side-menu-content.js b/ReactNativeClient/lib/components/side-menu-content.js index d4abbcca9..5724b393e 100644 --- a/ReactNativeClient/lib/components/side-menu-content.js +++ b/ReactNativeClient/lib/components/side-menu-content.js @@ -3,9 +3,10 @@ const { TouchableOpacity , Button, Text, Image, StyleSheet, ScrollView, View } = const { connect } = require('react-redux'); const Icon = require('react-native-vector-icons/Ionicons').default; const { Log } = require('lib/log.js'); -const { Tag } = require('lib/models/tag.js'); -const { Note } = require('lib/models/note.js'); -const { Setting } = require('lib/models/setting.js'); +const Tag = require('lib/models/Tag.js'); +const Note = require('lib/models/Note.js'); +const Folder = require('lib/models/Folder.js'); +const Setting = require('lib/models/Setting.js'); const { FoldersScreenUtils } = require('lib/folders-screen-utils.js'); const { Synchronizer } = require('lib/synchronizer.js'); const { reg } = require('lib/registry.js'); @@ -117,7 +118,7 @@ class SideMenuContentComponent extends Component { { this.folder_press(folder) }}> { iconComp } - {folder.title} + {Folder.displayTitle(folder)} ); @@ -131,7 +132,7 @@ class SideMenuContentComponent extends Component { { this.tag_press(tag) }}> { iconComp } - {tag.title} + {Tag.displayTitle(tag)} ); diff --git a/ReactNativeClient/lib/database.js b/ReactNativeClient/lib/database.js index 940bac41e..a601a032a 100644 --- a/ReactNativeClient/lib/database.js +++ b/ReactNativeClient/lib/database.js @@ -104,29 +104,38 @@ class Database { return this.tryCall('exec', sql, params); } - transactionExecBatch(queries) { - if (queries.length <= 0) return Promise.resolve(); + async transactionExecBatch(queries) { + if (queries.length <= 0) return; if (queries.length == 1) { let q = this.wrapQuery(queries[0]); - return this.exec(q.sql, q.params); + await this.exec(q.sql, q.params); + return; } // There can be only one transaction running at a time so queue // any new transaction here. if (this.inTransaction_) { - return new Promise((resolve, reject) => { - let iid = setInterval(() => { - if (!this.inTransaction_) { - clearInterval(iid); - this.transactionExecBatch(queries).then(() => { - resolve(); - }).catch((error) => { - reject(error); - }); - } - }, 100); - }); + while (true) { + await time.msleep(100); + if (!this.inTransaction_) { + this.inTransaction_ = true; + break; + } + } + + // return new Promise((resolve, reject) => { + // let iid = setInterval(() => { + // if (!this.inTransaction_) { + // clearInterval(iid); + // this.transactionExecBatch(queries).then(() => { + // resolve(); + // }).catch((error) => { + // reject(error); + // }); + // } + // }, 100); + // }); } this.inTransaction_ = true; @@ -134,17 +143,62 @@ class Database { queries.splice(0, 0, 'BEGIN TRANSACTION'); queries.push('COMMIT'); // Note: ROLLBACK is currently not supported - let chain = []; for (let i = 0; i < queries.length; i++) { let query = this.wrapQuery(queries[i]); - chain.push(() => { - return this.exec(query.sql, query.params); - }); + await this.exec(query.sql, query.params); } - return promiseChain(chain).then(() => { - this.inTransaction_ = false; - }); + this.inTransaction_ = false; + + // return promiseChain(chain).then(() => { + // this.inTransaction_ = false; + // }); + + + + + + + // if (queries.length <= 0) return Promise.resolve(); + + // if (queries.length == 1) { + // let q = this.wrapQuery(queries[0]); + // return this.exec(q.sql, q.params); + // } + + // // There can be only one transaction running at a time so queue + // // any new transaction here. + // if (this.inTransaction_) { + // return new Promise((resolve, reject) => { + // let iid = setInterval(() => { + // if (!this.inTransaction_) { + // clearInterval(iid); + // this.transactionExecBatch(queries).then(() => { + // resolve(); + // }).catch((error) => { + // reject(error); + // }); + // } + // }, 100); + // }); + // } + + // this.inTransaction_ = true; + + // queries.splice(0, 0, 'BEGIN TRANSACTION'); + // queries.push('COMMIT'); // Note: ROLLBACK is currently not supported + + // let chain = []; + // for (let i = 0; i < queries.length; i++) { + // let query = this.wrapQuery(queries[i]); + // chain.push(() => { + // return this.exec(query.sql, query.params); + // }); + // } + + // return promiseChain(chain).then(() => { + // this.inTransaction_ = false; + // }); } static enumId(type, s) { @@ -153,6 +207,7 @@ class Database { if (s == 'string') return 2; } if (type == 'fieldType') { + if (s) s = s.toUpperCase(); if (s == 'INTEGER') s = 'INT'; if (!(('TYPE_' + s) in this)) throw new Error('Unkonwn fieldType: ' + s); return this['TYPE_' + s]; diff --git a/ReactNativeClient/lib/file-api-driver-local.js b/ReactNativeClient/lib/file-api-driver-local.js index 3c6b605a7..326fda53c 100644 --- a/ReactNativeClient/lib/file-api-driver-local.js +++ b/ReactNativeClient/lib/file-api-driver-local.js @@ -1,7 +1,7 @@ const fs = require('fs-extra'); const { promiseChain } = require('lib/promise-utils.js'); const moment = require('moment'); -const { BaseItem } = require('lib/models/base-item.js'); +const BaseItem = require('lib/models/BaseItem.js'); const { time } = require('lib/time-utils.js'); // NOTE: when synchronising with the file system the time resolution is the second (unlike milliseconds for OneDrive for instance). @@ -146,10 +146,10 @@ class FileApiDriverLocal { let output = null; try { - if (options.encoding == 'binary') { - output = fs.readFile(path); + if (options.target === 'file') { + output = await fs.copy(path, options.path, { overwrite: true }); } else { - output = fs.readFile(path, options.encoding); + output = await fs.readFile(path, options.encoding); } } catch (error) { if (error.code == 'ENOENT') return null; @@ -178,7 +178,11 @@ class FileApiDriverLocal { }); } - put(path, content) { + async put(path, content, options = null) { + if (!options) options = {}; + + if (options.source === 'file') content = await fs.readFile(options.path); + return new Promise((resolve, reject) => { fs.writeFile(path, content, function(error) { if (error) { diff --git a/ReactNativeClient/lib/file-api-driver-memory.js b/ReactNativeClient/lib/file-api-driver-memory.js index a7878fe26..317796ec2 100644 --- a/ReactNativeClient/lib/file-api-driver-memory.js +++ b/ReactNativeClient/lib/file-api-driver-memory.js @@ -1,4 +1,5 @@ const { time } = require('lib/time-utils.js'); +const fs = require('fs-extra'); class FileApiDriverMemory { @@ -7,6 +8,18 @@ class FileApiDriverMemory { this.deletedItems_ = []; } + encodeContent_(content) { + if (content instanceof Buffer) { + return content.toString('base64'); + } else { + return Buffer.from(content).toString('base64'); + } + } + + decodeContent_(content) { + return Buffer.from(content, 'base64').toString('ascii'); + } + itemIndexByPath(path) { for (let i = 0; i < this.items_.length; i++) { if (this.items_[i].path == path) return i; @@ -65,11 +78,20 @@ class FileApiDriverMemory { }); } - get(path) { + async get(path, options) { let item = this.itemByPath(path); if (!item) return Promise.resolve(null); if (item.isDir) return Promise.reject(new Error(path + ' is a directory, not a file')); - return Promise.resolve(item.content); + + let output = null; + if (options.target === 'file') { + await fs.writeFile(options.path, Buffer.from(item.content, 'base64')); + } else { + const content = this.decodeContent_(item.content); + output = Promise.resolve(content); + } + + return output; } mkdir(path) { @@ -79,14 +101,18 @@ class FileApiDriverMemory { return Promise.resolve(); } - put(path, content) { + async put(path, content, options = null) { + if (!options) options = {}; + + if (options.source === 'file') content = await fs.readFile(options.path); + let index = this.itemIndexByPath(path); if (index < 0) { let item = this.newItem(path, false); - item.content = content; + item.content = this.encodeContent_(content); this.items_.push(item); } else { - this.items_[index].content = content; + this.items_[index].content = this.encodeContent_(content); this.items_[index].updated_time = time.unix(); } return Promise.resolve(); diff --git a/ReactNativeClient/lib/file-api-driver-onedrive.js b/ReactNativeClient/lib/file-api-driver-onedrive.js index 72ac30930..155a2c185 100644 --- a/ReactNativeClient/lib/file-api-driver-onedrive.js +++ b/ReactNativeClient/lib/file-api-driver-onedrive.js @@ -137,7 +137,7 @@ class FileApiDriverOneDrive { } } catch (error) { if (error && error.code === 'BadRequest' && error.message === 'Maximum request length exceeded.') { - error.code = 'cannotSync'; + error.code = 'rejectedByTarget'; error.message = 'Resource exceeds OneDrive max file size (4MB)'; } throw error; diff --git a/ReactNativeClient/lib/folders-screen-utils.js b/ReactNativeClient/lib/folders-screen-utils.js index 2a7f348ff..1a6971da4 100644 --- a/ReactNativeClient/lib/folders-screen-utils.js +++ b/ReactNativeClient/lib/folders-screen-utils.js @@ -1,4 +1,4 @@ -const { Folder } = require('lib/models/folder.js'); +const Folder = require('lib/models/Folder.js'); class FoldersScreenUtils { @@ -7,7 +7,7 @@ class FoldersScreenUtils { this.dispatch({ type: 'FOLDER_UPDATE_ALL', - folders: initialFolders, + items: initialFolders, }); } diff --git a/ReactNativeClient/lib/fs-driver-node.js b/ReactNativeClient/lib/fs-driver-node.js index f47f84d15..d940907b6 100644 --- a/ReactNativeClient/lib/fs-driver-node.js +++ b/ReactNativeClient/lib/fs-driver-node.js @@ -6,15 +6,54 @@ class FsDriverNode { return fs.appendFileSync(path, string); } + appendFile(path, string, encoding = 'base64') { + return fs.appendFile(path, string, { encoding: encoding }); + } + writeBinaryFile(path, content) { let buffer = new Buffer(content); return fs.writeFile(path, buffer); } + move(source, dest) { + return fs.move(source, dest, { overwrite: true }); + } + + exists(path) { + return fs.pathExists(path); + } + + open(path, mode) { + return fs.open(path, mode); + } + + close(handle) { + return fs.close(handle); + } + readFile(path) { return fs.readFile(path); } + async unlink(path) { + try { + await fs.unlink(path); + } catch (error) { + if (error.code === 'ENOENT') return; // Don't throw if the file does not exist + throw error; + } + } + + async readFileChunk(handle, length, encoding = 'base64') { + let buffer = new Buffer(length); + const result = await fs.read(handle, buffer, 0, length, null); + if (!result.bytesRead) return null; + buffer = buffer.slice(0, result.bytesRead); + if (encoding === 'base64') return buffer.toString('base64'); + if (encoding === 'ascii') return buffer.toString('ascii'); + throw new Error('Unsupported encoding: ' + encoding); + } + } module.exports.FsDriverNode = FsDriverNode; \ No newline at end of file diff --git a/ReactNativeClient/lib/fs-driver-rn.js b/ReactNativeClient/lib/fs-driver-rn.js new file mode 100644 index 000000000..a92315cd2 --- /dev/null +++ b/ReactNativeClient/lib/fs-driver-rn.js @@ -0,0 +1,72 @@ +const RNFS = require('react-native-fs'); + +class FsDriverRN { + + appendFileSync(path, string) { + throw new Error('Not implemented'); + } + + appendFile(path, string, encoding = 'base64') { + return RNFS.appendFile(path, string, encoding); + } + + writeBinaryFile(path, content) { + throw new Error('Not implemented'); + } + + async move(source, dest) { + return RNFS.moveFile(source, dest); + } + + async exists(path) { + return RNFS.exists(path); + } + + async open(path, mode) { + // Note: RNFS.read() doesn't provide any way to know if the end of file has been reached. + // So instead we stat the file here and use stat.size to manually check for end of file. + // Bug: https://github.com/itinance/react-native-fs/issues/342 + const stat = await RNFS.stat(path); + return { + path: path, + offset: 0, + mode: mode, + stat: stat, + } + } + + close(handle) { + return null; + } + + readFile(path) { + throw new Error('Not implemented'); + } + + async unlink(path) { + try { + await RNFS.unlink(path); + } catch (error) { + if (error && error.message && error.message.indexOf('exist') >= 0) { + // Probably { [Error: File does not exist] framesToPop: 1, code: 'EUNSPECIFIED' } + // which unfortunately does not have a proper error code. Can be ignored. + } else { + throw error; + } + } + } + + async readFileChunk(handle, length, encoding = 'base64') { + if (handle.offset + length > handle.stat.size) { + length = handle.stat.size - handle.offset; + } + + if (!length) return null; + let output = await RNFS.read(handle.path, length, handle.offset, encoding); + handle.offset += length; + return output ? output : null; + } + +} + +module.exports.FsDriverRN = FsDriverRN; \ No newline at end of file diff --git a/ReactNativeClient/lib/geolocation-react.js b/ReactNativeClient/lib/geolocation-react.js index f06f8861f..dbb2ca1f5 100644 --- a/ReactNativeClient/lib/geolocation-react.js +++ b/ReactNativeClient/lib/geolocation-react.js @@ -1,4 +1,4 @@ -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); class GeolocationReact { diff --git a/ReactNativeClient/lib/import-enex.js b/ReactNativeClient/lib/import-enex.js index 5fa08ed2f..8ebada188 100644 --- a/ReactNativeClient/lib/import-enex.js +++ b/ReactNativeClient/lib/import-enex.js @@ -2,11 +2,11 @@ const { uuid } = require('lib/uuid.js'); const moment = require('moment'); const { promiseChain } = require('lib/promise-utils.js'); const { folderItemFilename } = require('lib/string-utils.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Note } = require('lib/models/note.js'); -const { Tag } = require('lib/models/tag.js'); -const { Resource } = require('lib/models/resource.js'); -const { Folder } = require('lib/models/folder.js'); +const BaseModel = require('lib/BaseModel.js'); +const Note = require('lib/models/Note.js'); +const Tag = require('lib/models/Tag.js'); +const Resource = require('lib/models/Resource.js'); +const Folder = require('lib/models/Folder.js'); const { enexXmlToMd } = require('./import-enex-md-gen.js'); const { time } = require('lib/time-utils.js'); const Levenshtein = require('levenshtein'); diff --git a/ReactNativeClient/lib/joplin-database.js b/ReactNativeClient/lib/joplin-database.js index dfe0e2e6a..382cdcac4 100644 --- a/ReactNativeClient/lib/joplin-database.js +++ b/ReactNativeClient/lib/joplin-database.js @@ -202,13 +202,16 @@ class JoplinDatabase extends Database { // default value and thus might cause problems. In that case, the default value // must be set in the synchronizer too. - const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8]; + const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion); + + if (currentVersionIndex < 0) throw new Error('Unknown profile version. Most likely this is an old version of Joplin, while the profile was created by a newer version. Please upgrade Joplin at http://joplin.cozic.net and try again.'); + // currentVersionIndex < 0 if for the case where an old version of Joplin used with a newer // version of the database, so that migration is not run in this case. - if (currentVersionIndex == existingDatabaseVersions.length - 1 || currentVersionIndex < 0) return false; - + if (currentVersionIndex == existingDatabaseVersions.length - 1) return false; + while (currentVersionIndex < existingDatabaseVersions.length - 1) { const targetVersion = existingDatabaseVersions[currentVersionIndex + 1]; this.logger().info("Converting database to version " + targetVersion); @@ -270,6 +273,31 @@ class JoplinDatabase extends Database { queries.push('ALTER TABLE sync_items ADD COLUMN sync_disabled_reason TEXT NOT NULL DEFAULT ""'); } + if (targetVersion == 9) { + const newTableSql = ` + CREATE TABLE master_keys ( + id TEXT PRIMARY KEY, + created_time INT NOT NULL, + updated_time INT NOT NULL, + source_application TEXT NOT NULL, + encryption_method INT NOT NULL, + checksum TEXT NOT NULL, + content TEXT NOT NULL + ); + `; + queries.push(this.sqlStringToLines(newTableSql)[0]); + const tableNames = ['notes', 'folders', 'tags', 'note_tags', 'resources']; + for (let i = 0; i < tableNames.length; i++) { + const n = tableNames[i]; + queries.push('ALTER TABLE ' + n + ' ADD COLUMN encryption_cipher_text TEXT NOT NULL DEFAULT ""'); + queries.push('ALTER TABLE ' + n + ' ADD COLUMN encryption_applied INT NOT NULL DEFAULT 0'); + queries.push('CREATE INDEX ' + n + '_encryption_applied ON ' + n + ' (encryption_applied)'); + } + + queries.push('ALTER TABLE sync_items ADD COLUMN force_sync INT NOT NULL DEFAULT 0'); + queries.push('ALTER TABLE resources ADD COLUMN encryption_blob_encrypted INT NOT NULL DEFAULT 0'); + } + queries.push({ sql: 'UPDATE version SET version = ?', params: [targetVersion] }); await this.transactionExecBatch(queries); diff --git a/ReactNativeClient/lib/locale.js b/ReactNativeClient/lib/locale.js index 407fba8f4..6a94aa096 100644 --- a/ReactNativeClient/lib/locale.js +++ b/ReactNativeClient/lib/locale.js @@ -172,6 +172,7 @@ codeToLanguage_["hu"] = "Magyar"; let codeToCountry_ = {}; codeToCountry_["BR"] = "Brasil"; +codeToCountry_["CR"] = "Costa Rica"; codeToCountry_["CN"] = "中国"; let supportedLocales_ = null; diff --git a/ReactNativeClient/lib/models/Alarm.js b/ReactNativeClient/lib/models/Alarm.js index b7095c288..30e0fdc0a 100644 --- a/ReactNativeClient/lib/models/Alarm.js +++ b/ReactNativeClient/lib/models/Alarm.js @@ -1,5 +1,5 @@ -const { BaseModel } = require('lib/base-model.js'); -const { Note } = require('lib/models/note.js'); +const BaseModel = require('lib/BaseModel.js'); +const Note = require('lib/models/Note.js'); class Alarm extends BaseModel { diff --git a/ReactNativeClient/lib/models/base-item.js b/ReactNativeClient/lib/models/BaseItem.js similarity index 68% rename from ReactNativeClient/lib/models/base-item.js rename to ReactNativeClient/lib/models/BaseItem.js index 1066a0eaa..0619b785a 100644 --- a/ReactNativeClient/lib/models/base-item.js +++ b/ReactNativeClient/lib/models/BaseItem.js @@ -1,8 +1,10 @@ -const { BaseModel } = require('lib/base-model.js'); +const BaseModel = require('lib/BaseModel.js'); const { Database } = require('lib/database.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); +const JoplinError = require('lib/JoplinError.js'); const { time } = require('lib/time-utils.js'); const { sprintf } = require('sprintf-js'); +const { _ } = require('lib/locale.js'); const moment = require('moment'); class BaseItem extends BaseModel { @@ -11,6 +13,10 @@ class BaseItem extends BaseModel { return true; } + static encryptionSupported() { + return true; + } + static loadClass(className, classRef) { for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) { if (BaseItem.syncItemDefinitions_[i].className == className) { @@ -26,6 +32,8 @@ class BaseItem extends BaseModel { static getClass(name) { for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) { if (BaseItem.syncItemDefinitions_[i].className == name) { + const classRef = BaseItem.syncItemDefinitions_[i].classRef; + if (!classRef) throw new Error('Class has not been loaded: ' + name); return BaseItem.syncItemDefinitions_[i].classRef; } } @@ -245,6 +253,55 @@ class BaseItem extends BaseModel { return temp.join("\n\n"); } + static encryptionService() { + if (!this.encryptionService_) throw new Error('BaseItem.encryptionService_ is not set!!'); + return this.encryptionService_; + } + + static async serializeForSync(item) { + const ItemClass = this.itemClass(item); + let serialized = await ItemClass.serialize(item); + if (!Setting.value('encryption.enabled') || !ItemClass.encryptionSupported()) { + // Normally not possible since itemsThatNeedSync should only return decrypted items + if (!!item.encryption_applied) throw new JoplinError('Item is encrypted but encryption is currently disabled', 'cannotSyncEncrypted'); + return serialized; + } + + if (!!item.encryption_applied) { const e = new Error('Trying to encrypt item that is already encrypted'); e.code = 'cannotEncryptEncrypted'; throw e; } + + const cipherText = await this.encryptionService().encryptString(serialized); + + // List of keys that won't be encrypted - mostly foreign keys required to link items + // with each others and timestamp required for synchronisation. + const keepKeys = ['id', 'note_id', 'tag_id', 'parent_id', 'updated_time', 'type_']; + const reducedItem = {}; + + for (let i = 0; i < keepKeys.length; i++) { + const n = keepKeys[i]; + if (!item.hasOwnProperty(n)) continue; + reducedItem[n] = item[n]; + } + + reducedItem.encryption_applied = 1; + reducedItem.encryption_cipher_text = cipherText; + + return ItemClass.serialize(reducedItem) + } + + static async decrypt(item) { + if (!item.encryption_cipher_text) throw new Error('Item is not encrypted: ' + item.id); + + const ItemClass = this.itemClass(item); + const plainText = await this.encryptionService().decryptString(item.encryption_cipher_text); + + // Note: decryption does not count has a change, so don't update any timestamp + const plainItem = await ItemClass.unserialize(plainText); + plainItem.updated_time = item.updated_time; + plainItem.encryption_cipher_text = ''; + plainItem.encryption_applied = 0; + return ItemClass.save(plainItem, { autoTimestamp: false }); + } + static async unserialize(content) { let lines = content.split("\n"); let output = {}; @@ -290,6 +347,84 @@ class BaseItem extends BaseModel { return output; } + static async encryptedItemsStats() { + const classNames = this.encryptableItemClassNames(); + let encryptedCount = 0; + let totalCount = 0; + + for (let i = 0; i < classNames.length; i++) { + const ItemClass = this.getClass(classNames[i]); + encryptedCount += await ItemClass.count({ where: 'encryption_applied = 1' }); + totalCount += await ItemClass.count(); + } + + return { + encrypted: encryptedCount, + total: totalCount, + }; + } + + static async encryptedItemsCount() { + const classNames = this.encryptableItemClassNames(); + let output = 0; + + for (let i = 0; i < classNames.length; i++) { + const className = classNames[i]; + const ItemClass = this.getClass(className); + const count = await ItemClass.count({ where: 'encryption_applied = 1' }); + output += count; + } + + return output; + } + + static async hasEncryptedItems() { + const classNames = this.encryptableItemClassNames(); + + for (let i = 0; i < classNames.length; i++) { + const className = classNames[i]; + const ItemClass = this.getClass(className); + + const count = await ItemClass.count({ where: 'encryption_applied = 1' }); + if (count) return true; + } + + return false; + } + + static async itemsThatNeedDecryption(exclusions = [], limit = 100) { + const classNames = this.encryptableItemClassNames(); + + for (let i = 0; i < classNames.length; i++) { + const className = classNames[i]; + const ItemClass = this.getClass(className); + + const whereSql = className === 'Resource' ? ['(encryption_blob_encrypted = 1 OR encryption_applied = 1)'] : ['encryption_applied = 1']; + if (exclusions.length) whereSql.push('id NOT IN ("' + exclusions.join('","') + '")'); + + const sql = sprintf(` + SELECT * + FROM %s + WHERE %s + LIMIT %d + `, + this.db().escapeField(ItemClass.tableName()), + whereSql.join(' AND '), + limit + ); + + const items = await ItemClass.modelSelectAll(sql); + + if (i >= classNames.length - 1) { + return { hasMore: items.length >= limit, items: items }; + } else { + if (items.length) return { hasMore: true, items: items }; + } + } + + throw new Error('Unreachable'); + } + static async itemsThatNeedSync(syncTarget, limit = 100) { const classNames = this.syncItemClassNames(); @@ -304,7 +439,12 @@ class BaseItem extends BaseModel { // // CHANGED: // 'SELECT * FROM [ITEMS] items JOIN sync_items s ON s.item_id = items.id WHERE sync_target = ? AND' - let extraWhere = className == 'Note' ? 'AND is_conflict = 0' : ''; + let extraWhere = []; + if (className == 'Note') extraWhere.push('is_conflict = 0'); + if (className == 'Resource') extraWhere.push('encryption_blob_encrypted = 0'); + if (ItemClass.encryptionSupported()) extraWhere.push('encryption_applied = 0'); + + extraWhere = extraWhere.length ? 'AND ' + extraWhere.join(' AND ') : ''; // First get all the items that have never been synced under this sync target @@ -338,7 +478,7 @@ class BaseItem extends BaseModel { SELECT %s FROM %s items JOIN sync_items s ON s.item_id = items.id WHERE sync_target = %d - AND s.sync_time < items.updated_time + AND (s.sync_time < items.updated_time OR force_sync = 1) AND s.sync_disabled = 0 %s LIMIT %d @@ -370,6 +510,16 @@ class BaseItem extends BaseModel { }); } + static encryptableItemClassNames() { + const temp = this.syncItemClassNames(); + let output = []; + for (let i = 0; i < temp.length; i++) { + if (temp[i] === 'MasterKey') continue; + output.push(temp[i]); + } + return output; + } + static syncItemTypes() { return BaseItem.syncItemDefinitions_.map((def) => { return def.type; @@ -445,8 +595,55 @@ class BaseItem extends BaseModel { await this.db().transactionExecBatch(queries); } + static displayTitle(item) { + if (!item) return ''; + return !!item.encryption_applied ? '🔑 ' + _('Encrypted') : item.title + ''; + } + + static async markAllNonEncryptedForSync() { + const classNames = this.encryptableItemClassNames(); + + for (let i = 0; i < classNames.length; i++) { + const className = classNames[i]; + const ItemClass = this.getClass(className); + + const sql = sprintf(` + SELECT id + FROM %s + WHERE encryption_applied = 0`, + this.db().escapeField(ItemClass.tableName()), + ); + + const items = await ItemClass.modelSelectAll(sql); + const ids = items.map((item) => {return item.id}); + if (!ids.length) continue; + + await this.db().exec('UPDATE sync_items SET force_sync = 1 WHERE item_id IN ("' + ids.join('","') + '")'); + } + } + + static async forceSync(itemId) { + await this.db().exec('UPDATE sync_items SET force_sync = 1 WHERE item_id = ?', [itemId]); + } + + static async forceSyncAll() { + await this.db().exec('UPDATE sync_items SET force_sync = 1'); + } + + static async save(o, options = null) { + if (!options) options = {}; + + if (options.userSideValidation === true) { + if (!!o.encryption_applied) throw new Error(_('Encrypted items cannot be modified')); + } + + return super.save(o, options); + } + } +BaseItem.encryptionService_ = null; + // Also update: // - itemsThatNeedSync() // - syncedItems() @@ -457,6 +654,7 @@ BaseItem.syncItemDefinitions_ = [ { type: BaseModel.TYPE_RESOURCE, className: 'Resource' }, { type: BaseModel.TYPE_TAG, className: 'Tag' }, { type: BaseModel.TYPE_NOTE_TAG, className: 'NoteTag' }, + { type: BaseModel.TYPE_MASTER_KEY, className: 'MasterKey' }, ]; -module.exports = { BaseItem }; \ No newline at end of file +module.exports = BaseItem; \ No newline at end of file diff --git a/ReactNativeClient/lib/models/folder.js b/ReactNativeClient/lib/models/Folder.js similarity index 94% rename from ReactNativeClient/lib/models/folder.js rename to ReactNativeClient/lib/models/Folder.js index a7d68d915..1e06c1367 100644 --- a/ReactNativeClient/lib/models/folder.js +++ b/ReactNativeClient/lib/models/Folder.js @@ -1,13 +1,13 @@ -const { BaseModel } = require('lib/base-model.js'); +const BaseModel = require('lib/BaseModel.js'); const { Log } = require('lib/log.js'); const { promiseChain } = require('lib/promise-utils.js'); const { time } = require('lib/time-utils.js'); -const { Note } = require('lib/models/note.js'); -const { Setting } = require('lib/models/setting.js'); +const Note = require('lib/models/Note.js'); +const Setting = require('lib/models/Setting.js'); const { Database } = require('lib/database.js'); const { _ } = require('lib/locale.js'); const moment = require('moment'); -const { BaseItem } = require('lib/models/base-item.js'); +const BaseItem = require('lib/models/BaseItem.js'); const lodash = require('lodash'); class Folder extends BaseItem { @@ -157,7 +157,7 @@ class Folder extends BaseItem { return super.save(o, options).then((folder) => { this.dispatch({ type: 'FOLDER_UPDATE_ONE', - folder: folder, + item: folder, }); return folder; }); @@ -165,4 +165,4 @@ class Folder extends BaseItem { } -module.exports = { Folder }; \ No newline at end of file +module.exports = Folder; \ No newline at end of file diff --git a/ReactNativeClient/lib/models/MasterKey.js b/ReactNativeClient/lib/models/MasterKey.js new file mode 100644 index 000000000..a3e5322e6 --- /dev/null +++ b/ReactNativeClient/lib/models/MasterKey.js @@ -0,0 +1,40 @@ +const BaseModel = require('lib/BaseModel.js'); +const BaseItem = require('lib/models/BaseItem.js'); + +class MasterKey extends BaseItem { + + static tableName() { + return 'master_keys'; + } + + static modelType() { + return BaseModel.TYPE_MASTER_KEY; + } + + static encryptionSupported() { + return false; + } + + static latest() { + return this.modelSelectOne('SELECT * FROM master_keys WHERE created_time >= (SELECT max(created_time) FROM master_keys)'); + } + + static async serialize(item, type = null, shownKeys = null) { + let fieldNames = this.fieldNames(); + fieldNames.push('type_'); + return super.serialize(item, 'master_key', fieldNames); + } + + static async save(o, options = null) { + return super.save(o, options).then((item) => { + this.dispatch({ + type: 'MASTERKEY_UPDATE_ONE', + item: item, + }); + return item; + }); + } + +} + +module.exports = MasterKey; \ No newline at end of file diff --git a/ReactNativeClient/lib/models/note.js b/ReactNativeClient/lib/models/Note.js similarity index 96% rename from ReactNativeClient/lib/models/note.js rename to ReactNativeClient/lib/models/Note.js index f5359fbdc..71bc5a377 100644 --- a/ReactNativeClient/lib/models/note.js +++ b/ReactNativeClient/lib/models/Note.js @@ -1,8 +1,8 @@ -const { BaseModel } = require('lib/base-model.js'); +const BaseModel = require('lib/BaseModel.js'); const { Log } = require('lib/log.js'); const { sprintf } = require('sprintf-js'); -const { BaseItem } = require('lib/models/base-item.js'); -const { Setting } = require('lib/models/setting.js'); +const BaseItem = require('lib/models/BaseItem.js'); +const Setting = require('lib/models/Setting.js'); const { shim } = require('lib/shim.js'); const { time } = require('lib/time-utils.js'); const { _ } = require('lib/locale.js'); @@ -153,7 +153,7 @@ class Note extends BaseItem { } static previewFields() { - return ['id', 'title', 'body', 'is_todo', 'todo_completed', 'parent_id', 'updated_time', 'user_updated_time']; + return ['id', 'title', 'body', 'is_todo', 'todo_completed', 'parent_id', 'updated_time', 'user_updated_time', 'encryption_applied']; } static previewFieldsSql() { @@ -438,6 +438,10 @@ class Note extends BaseItem { // That shouldn't happen so throw an exception if (localNote.id !== remoteNote.id) throw new Error('Cannot handle conflict for two different notes'); + // For encrypted notes the conflict must always be handled + if (localNote.encryption_cipher_text || remoteNote.encryption_cipher_text) return true; + + // Otherwise only handle the conflict if there's a different on the title or body if (localNote.title !== remoteNote.title) return true; if (localNote.body !== remoteNote.body) return true; @@ -449,4 +453,4 @@ class Note extends BaseItem { Note.updateGeolocationEnabled_ = true; Note.geolocationUpdating_ = false; -module.exports = { Note }; \ No newline at end of file +module.exports = Note; \ No newline at end of file diff --git a/ReactNativeClient/lib/models/note-tag.js b/ReactNativeClient/lib/models/NoteTag.js similarity index 84% rename from ReactNativeClient/lib/models/note-tag.js rename to ReactNativeClient/lib/models/NoteTag.js index b7808e531..b2de61265 100644 --- a/ReactNativeClient/lib/models/note-tag.js +++ b/ReactNativeClient/lib/models/NoteTag.js @@ -1,5 +1,5 @@ -const { BaseItem } = require('lib/models/base-item.js'); -const { BaseModel } = require('lib/base-model.js'); +const BaseItem = require('lib/models/BaseItem.js'); +const BaseModel = require('lib/BaseModel.js'); class NoteTag extends BaseItem { @@ -33,4 +33,4 @@ class NoteTag extends BaseItem { } -module.exports = { NoteTag }; \ No newline at end of file +module.exports = NoteTag; \ No newline at end of file diff --git a/ReactNativeClient/lib/models/Resource.js b/ReactNativeClient/lib/models/Resource.js new file mode 100644 index 000000000..7e6057258 --- /dev/null +++ b/ReactNativeClient/lib/models/Resource.js @@ -0,0 +1,143 @@ +const BaseModel = require('lib/BaseModel.js'); +const BaseItem = require('lib/models/BaseItem.js'); +const Setting = require('lib/models/Setting.js'); +const ArrayUtils = require('lib/ArrayUtils.js'); +const pathUtils = require('lib/path-utils.js'); +const { mime } = require('lib/mime-utils.js'); +const { filename } = require('lib/path-utils.js'); +const { FsDriverDummy } = require('lib/fs-driver-dummy.js'); +const { markdownUtils } = require('lib/markdown-utils.js'); + +class Resource extends BaseItem { + + static tableName() { + return 'resources'; + } + + static modelType() { + return BaseModel.TYPE_RESOURCE; + } + + static encryptionService() { + if (!this.encryptionService_) throw new Error('Resource.encryptionService_ is not set!!'); + return this.encryptionService_; + } + + static isSupportedImageMimeType(type) { + const imageMimeTypes = ["image/jpg", "image/jpeg", "image/png", "image/gif"]; + return imageMimeTypes.indexOf(type.toLowerCase()) >= 0; + } + + static fsDriver() { + if (!Resource.fsDriver_) Resource.fsDriver_ = new FsDriverDummy(); + return Resource.fsDriver_; + } + + static async serialize(item, type = null, shownKeys = null) { + let fieldNames = this.fieldNames(); + fieldNames.push('type_'); + //fieldNames = ArrayUtils.removeElement(fieldNames, 'encryption_blob_encrypted'); + return super.serialize(item, 'resource', fieldNames); + } + + static filename(resource, encryptedBlob = false) { + let extension = encryptedBlob ? 'crypted' : resource.file_extension; + if (!extension) extension = resource.mime ? mime.toFileExtension(resource.mime) : ''; + extension = extension ? ('.' + extension) : ''; + return resource.id + extension; + } + + static fullPath(resource, encryptedBlob = false) { + return Setting.value('resourceDir') + '/' + this.filename(resource, encryptedBlob); + } + + // For resources, we need to decrypt the item (metadata) and the resource binary blob. + static async decrypt(item) { + // The item might already be decrypted but not the blob (for instance if it crashes while + // decrypting the blob or was otherwise interrupted). + const decryptedItem = item.encryption_cipher_text ? await super.decrypt(item) : Object.assign({}, item); + if (!decryptedItem.encryption_blob_encrypted) return decryptedItem; + + const plainTextPath = this.fullPath(decryptedItem); + const encryptedPath = this.fullPath(decryptedItem, true); + const noExtPath = pathUtils.dirname(encryptedPath) + '/' + pathUtils.filename(encryptedPath); + + // When the resource blob is downloaded by the synchroniser, it's initially a file with no + // extension (since it's encrypted, so we don't know its extension). So here rename it + // to a file with a ".crypted" extension so that it's better identified, and then decrypt it. + // Potentially plainTextPath is also a path with no extension if it's an unknown mime type. + if (await this.fsDriver().exists(noExtPath)) { + await this.fsDriver().move(noExtPath, encryptedPath); + } + + await this.encryptionService().decryptFile(encryptedPath, plainTextPath); + + decryptedItem.encryption_blob_encrypted = 0; + return super.save(decryptedItem, { autoTimestamp: false }); + } + + // Prepare the resource by encrypting it if needed. + // The call returns the path to the physical file AND a representation of the resource object + // as it should be uploaded to the sync target. Note that this may be different from what is stored + // in the database. In particular, the flag encryption_blob_encrypted might be 1 on the sync target + // if the resource is encrypted, but will be 0 locally because the device has the decrypted resource. + static async fullPathForSyncUpload(resource) { + const plainTextPath = this.fullPath(resource); + + if (!Setting.value('encryption.enabled')) { + // Normally not possible since itemsThatNeedSync should only return decrypted items + if (!!resource.encryption_blob_encrypted) throw new Error('Trying to access encrypted resource but encryption is currently disabled'); + return { path: plainTextPath, resource: resource }; + } + + const encryptedPath = this.fullPath(resource, true); + if (resource.encryption_blob_encrypted) return { path: encryptedPath, resource: resource }; + await this.encryptionService().encryptFile(plainTextPath, encryptedPath); + + const resourceCopy = Object.assign({}, resource); + resourceCopy.encryption_blob_encrypted = 1; + return { path: encryptedPath, resource: resourceCopy }; + } + + static markdownTag(resource) { + let tagAlt = resource.alt ? resource.alt : resource.title; + if (!tagAlt) tagAlt = ''; + let lines = []; + if (Resource.isSupportedImageMimeType(resource.mime)) { + lines.push("!["); + lines.push(markdownUtils.escapeLinkText(tagAlt)); + lines.push("](:/" + resource.id + ")"); + } else { + lines.push("["); + lines.push(markdownUtils.escapeLinkText(tagAlt)); + lines.push("](:/" + resource.id + ")"); + } + return lines.join(''); + } + + static pathToId(path) { + return filename(path); + } + + static async content(resource) { + return this.fsDriver().readFile(this.fullPath(resource)); + } + + static setContent(resource, content) { + return this.fsDriver().writeBinaryFile(this.fullPath(resource), content); + } + + static isResourceUrl(url) { + return url && url.length === 34 && url[0] === ':' && url[1] === '/'; + } + + static urlToId(url) { + if (!this.isResourceUrl(url)) throw new Error('Not a valid resource URL: ' + url); + return url.substr(2); + } + +} + +Resource.IMAGE_MAX_DIMENSION = 1920; + +module.exports = Resource; \ No newline at end of file diff --git a/ReactNativeClient/lib/models/setting.js b/ReactNativeClient/lib/models/Setting.js similarity index 91% rename from ReactNativeClient/lib/models/setting.js rename to ReactNativeClient/lib/models/Setting.js index 0dceaf719..7fb96aec4 100644 --- a/ReactNativeClient/lib/models/setting.js +++ b/ReactNativeClient/lib/models/Setting.js @@ -1,4 +1,4 @@ -const { BaseModel } = require('lib/base-model.js'); +const BaseModel = require('lib/BaseModel.js'); const { Database } = require('lib/database.js'); const { Logger } = require('lib/logger.js'); const SyncTargetRegistry = require('lib/SyncTargetRegistry.js'); @@ -60,6 +60,9 @@ class Setting extends BaseModel { // })}, 'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Show uncompleted todos on top of the lists') }, 'trackLocation': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Save geo-location with notes') }, + 'encryption.enabled': { value: false, type: Setting.TYPE_BOOL, public: false }, + 'encryption.activeMasterKeyId': { value: '', type: Setting.TYPE_STRING, public: false }, + 'encryption.passwordCache': { value: {}, type: Setting.TYPE_OBJECT, public: false }, 'sync.interval': { value: 300, type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation interval'), options: () => { return { 0: _('Disabled'), @@ -218,6 +221,20 @@ class Setting extends BaseModel { this.scheduleSave(); } + static setObjectKey(settingKey, objectKey, value) { + let o = this.value(settingKey); + if (typeof o !== 'object') o = {}; + o[objectKey] = value; + this.setValue(settingKey, o); + } + + static deleteObjectKey(settingKey, objectKey) { + const o = this.value(settingKey); + if (typeof o !== 'object') return; + delete o[objectKey]; + this.setValue(settingKey, o); + } + static valueToString(key, value) { const md = this.settingMetadata(key); value = this.formatValue(key, value); @@ -261,6 +278,16 @@ class Setting extends BaseModel { } static value(key) { + // Need to copy arrays and objects since in setValue(), the old value and new one is compared + // with strict equality and the value is updated only if changed. However if the caller acquire + // and object and change a key, the objects will be detected as equal. By returning a copy + // we avoid this problem. + function copyIfNeeded(value) { + if (Array.isArray(value)) return value.slice(); + if (typeof value === 'object') return Object.assign({}, value); + return value; + } + if (key in this.constants_) { const v = this.constants_[key]; const output = typeof v === 'function' ? v() : v; @@ -272,12 +299,12 @@ class Setting extends BaseModel { for (let i = 0; i < this.cache_.length; i++) { if (this.cache_[i].key == key) { - return this.cache_[i].value; + return copyIfNeeded(this.cache_[i].value); } } const md = this.settingMetadata(key); - return md.value; + return copyIfNeeded(md.value); } static isEnum(key) { @@ -439,4 +466,4 @@ Setting.constants_ = { openDevTools: false, } -module.exports = { Setting }; \ No newline at end of file +module.exports = Setting; \ No newline at end of file diff --git a/ReactNativeClient/lib/models/tag.js b/ReactNativeClient/lib/models/Tag.js similarity index 91% rename from ReactNativeClient/lib/models/tag.js rename to ReactNativeClient/lib/models/Tag.js index 8ff5f1d6b..806ae302d 100644 --- a/ReactNativeClient/lib/models/tag.js +++ b/ReactNativeClient/lib/models/Tag.js @@ -1,7 +1,7 @@ -const { BaseModel } = require('lib/base-model.js'); -const { BaseItem } = require('lib/models/base-item.js'); -const { NoteTag } = require('lib/models/note-tag.js'); -const { Note } = require('lib/models/note.js'); +const BaseModel = require('lib/BaseModel.js'); +const BaseItem = require('lib/models/BaseItem.js'); +const NoteTag = require('lib/models/NoteTag.js'); +const Note = require('lib/models/Note.js'); const { time } = require('lib/time-utils.js'); class Tag extends BaseItem { @@ -70,7 +70,7 @@ class Tag extends BaseItem { this.dispatch({ type: 'TAG_UPDATE_ONE', - tag: await Tag.load(tagId), + item: await Tag.load(tagId), }); return output; @@ -84,7 +84,7 @@ class Tag extends BaseItem { this.dispatch({ type: 'TAG_UPDATE_ONE', - tag: await Tag.load(tagId), + item: await Tag.load(tagId), }); } @@ -132,7 +132,7 @@ class Tag extends BaseItem { return super.save(o, options).then((tag) => { this.dispatch({ type: 'TAG_UPDATE_ONE', - tag: tag, + item: tag, }); return tag; }); @@ -140,4 +140,4 @@ class Tag extends BaseItem { } -module.exports = { Tag }; \ No newline at end of file +module.exports = Tag; \ No newline at end of file diff --git a/ReactNativeClient/lib/models/resource.js b/ReactNativeClient/lib/models/resource.js deleted file mode 100644 index ccd91bb84..000000000 --- a/ReactNativeClient/lib/models/resource.js +++ /dev/null @@ -1,87 +0,0 @@ -const { BaseModel } = require('lib/base-model.js'); -const { BaseItem } = require('lib/models/base-item.js'); -const { Setting } = require('lib/models/setting.js'); -const { mime } = require('lib/mime-utils.js'); -const { filename } = require('lib/path-utils.js'); -const { FsDriverDummy } = require('lib/fs-driver-dummy.js'); -const { markdownUtils } = require('lib/markdown-utils.js'); - -class Resource extends BaseItem { - - static tableName() { - return 'resources'; - } - - static modelType() { - return BaseModel.TYPE_RESOURCE; - } - - static isSupportedImageMimeType(type) { - const imageMimeTypes = ["image/jpg", "image/jpeg", "image/png", "image/gif"]; - return imageMimeTypes.indexOf(type.toLowerCase()) >= 0; - } - - static fsDriver() { - if (!Resource.fsDriver_) Resource.fsDriver_ = new FsDriverDummy(); - return Resource.fsDriver_; - } - - static async serialize(item, type = null, shownKeys = null) { - let fieldNames = this.fieldNames(); - fieldNames.push('type_'); - return super.serialize(item, 'resource', fieldNames); - } - - static filename(resource) { - let extension = resource.file_extension; - if (!extension) extension = resource.mime ? mime.toFileExtension(resource.mime) : ''; - extension = extension ? '.' + extension : ''; - return resource.id + extension; - } - - static fullPath(resource) { - return Setting.value('resourceDir') + '/' + this.filename(resource); - } - - static markdownTag(resource) { - let tagAlt = resource.alt ? resource.alt : resource.title; - if (!tagAlt) tagAlt = ''; - let lines = []; - if (Resource.isSupportedImageMimeType(resource.mime)) { - lines.push("!["); - lines.push(markdownUtils.escapeLinkText(tagAlt)); - lines.push("](:/" + resource.id + ")"); - } else { - lines.push("["); - lines.push(markdownUtils.escapeLinkText(tagAlt)); - lines.push("](:/" + resource.id + ")"); - } - return lines.join(''); - } - - static pathToId(path) { - return filename(path); - } - - static async content(resource) { - return this.fsDriver().readFile(this.fullPath(resource)); - } - - static setContent(resource, content) { - return this.fsDriver().writeBinaryFile(this.fullPath(resource), content); - } - - static isResourceUrl(url) { - return url && url.length === 34 && url[0] === ':' && url[1] === '/'; - } - - static urlToId(url) { - if (!this.isResourceUrl(url)) throw new Error('Not a valid resource URL: ' + url); - return url.substr(2); - } - -} - -Resource.IMAGE_MAX_DIMENSION = 1920; - -module.exports = { Resource }; \ No newline at end of file diff --git a/ReactNativeClient/lib/onedrive-api.js b/ReactNativeClient/lib/onedrive-api.js index 87ab05b13..7f76e88eb 100644 --- a/ReactNativeClient/lib/onedrive-api.js +++ b/ReactNativeClient/lib/onedrive-api.js @@ -216,7 +216,7 @@ class OneDriveApi { this.logger().info(error); await time.sleep((i + 1) * 3); continue; - } else if (error && error.error && error.error.code === 'resourceModified') { + } else if (error && (error.code === 'resourceModified' || (error.error && error.error.code === 'resourceModified'))) { // NOTE: not tested, very hard to reproduce and non-informative error message, but can be repeated // Error: ETag does not match current item's value diff --git a/ReactNativeClient/lib/parameters.js b/ReactNativeClient/lib/parameters.js index 91fd46684..53002131d 100644 --- a/ReactNativeClient/lib/parameters.js +++ b/ReactNativeClient/lib/parameters.js @@ -1,4 +1,4 @@ -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const parameters_ = {}; diff --git a/ReactNativeClient/lib/reducer.js b/ReactNativeClient/lib/reducer.js index bb6bcfe9c..90c95fbb9 100644 --- a/ReactNativeClient/lib/reducer.js +++ b/ReactNativeClient/lib/reducer.js @@ -1,5 +1,5 @@ -const { Note } = require('lib/models/note.js'); -const { Folder } = require('lib/models/folder.js'); +const Note = require('lib/models/Note.js'); +const Folder = require('lib/models/Folder.js'); const ArrayUtils = require('lib/ArrayUtils.js'); const defaultState = { @@ -8,6 +8,8 @@ const defaultState = { notesParentType: null, folders: [], tags: [], + masterKeys: [], + notLoadedMasterKeys: [], searches: [], selectedNoteIds: [], selectedFolderId: null, @@ -29,6 +31,20 @@ const defaultState = { hasDisabledSyncItems: false, }; +function arrayHasEncryptedItems(array) { + for (let i = 0; i < array.length; i++) { + if (!!array[i].encryption_applied) return true; + } + return false +} + +function stateHasEncryptedItems(state) { + if (arrayHasEncryptedItems(state.notes)) return true; + if (arrayHasEncryptedItems(state.folders)) return true; + if (arrayHasEncryptedItems(state.tags)) return true; + return false; +} + // When deleting a note, tag or folder function handleItemDelete(state, action) { let newState = Object.assign({}, state); @@ -72,9 +88,16 @@ function handleItemDelete(state, action) { return newState; } -function updateOneTagOrFolder(state, action) { - let newItems = action.type === 'TAG_UPDATE_ONE' ? state.tags.splice(0) : state.folders.splice(0); - let item = action.type === 'TAG_UPDATE_ONE' ? action.tag : action.folder; +function updateOneItem(state, action) { + // let newItems = action.type === 'TAG_UPDATE_ONE' ? state.tags.splice(0) : state.folders.splice(0); + // let item = action.type === 'TAG_UPDATE_ONE' ? action.tag : action.folder; + let itemsKey = null; + if (action.type === 'TAG_UPDATE_ONE') itemsKey = 'tags'; + if (action.type === 'FOLDER_UPDATE_ONE') itemsKey = 'folders'; + if (action.type === 'MASTERKEY_UPDATE_ONE') itemsKey = 'masterKeys'; + + let newItems = state[itemsKey].splice(0); + let item = action.item; var found = false; for (let i = 0; i < newItems.length; i++) { @@ -90,11 +113,13 @@ function updateOneTagOrFolder(state, action) { let newState = Object.assign({}, state); - if (action.type === 'TAG_UPDATE_ONE') { - newState.tags = newItems; - } else { - newState.folders = newItems; - } + newState[itemsKey] = newItems; + + // if (action.type === 'TAG_UPDATE_ONE') { + // newState.tags = newItems; + // } else { + // newState.folders = newItems; + // } return newState; } @@ -307,14 +332,14 @@ const reducer = (state = defaultState, action) => { case 'FOLDER_UPDATE_ALL': newState = Object.assign({}, state); - newState.folders = action.folders; + newState.folders = action.items; break; case 'TAG_UPDATE_ALL': newState = Object.assign({}, state); - newState.tags = action.tags; - break; + newState.tags = action.items; + break; case 'TAG_SELECT': @@ -328,13 +353,10 @@ const reducer = (state = defaultState, action) => { break; case 'TAG_UPDATE_ONE': - - newState = updateOneTagOrFolder(state, action); - break; - case 'FOLDER_UPDATE_ONE': + case 'MASTERKEY_UPDATE_ONE': - newState = updateOneTagOrFolder(state, action); + newState = updateOneItem(state, action); break; case 'FOLDER_DELETE': @@ -342,6 +364,37 @@ const reducer = (state = defaultState, action) => { newState = handleItemDelete(state, action); break; + case 'MASTERKEY_UPDATE_ALL': + + newState = Object.assign({}, state); + newState.masterKeys = action.items; + break; + + case 'MASTERKEY_ADD_NOT_LOADED': + + if (state.notLoadedMasterKeys.indexOf(action.id) < 0) { + newState = Object.assign({}, state); + const keys = newState.notLoadedMasterKeys.slice(); + keys.push(action.id); + newState.notLoadedMasterKeys = keys; + } + break; + + case 'MASTERKEY_REMOVE_NOT_LOADED': + + const ids = action.id ? [action.id] : action.ids; + for (let i = 0; i < ids.length; i++) { + const id = ids[i]; + const index = state.notLoadedMasterKeys.indexOf(id); + if (index >= 0) { + newState = Object.assign({}, state); + const keys = newState.notLoadedMasterKeys.slice(); + keys.splice(index, 1); + newState.notLoadedMasterKeys = keys; + } + } + break; + case 'SYNC_STARTED': newState = Object.assign({}, state); @@ -408,6 +461,11 @@ const reducer = (state = defaultState, action) => { throw error; } + if (action.type.indexOf('NOTE_UPDATE') === 0 || action.type.indexOf('FOLDER_UPDATE') === 0 || action.type.indexOf('TAG_UPDATE') === 0) { + newState = Object.assign({}, newState); + newState.hasEncryptedItems = stateHasEncryptedItems(newState); + } + return newState; } diff --git a/ReactNativeClient/lib/registry.js b/ReactNativeClient/lib/registry.js index fd624946e..1d7459b93 100644 --- a/ReactNativeClient/lib/registry.js +++ b/ReactNativeClient/lib/registry.js @@ -1,5 +1,5 @@ const { Logger } = require('lib/logger.js'); -const { Setting } = require('lib/models/setting.js'); +const Setting = require('lib/models/Setting.js'); const { shim } = require('lib/shim.js'); const SyncTargetRegistry = require('lib/SyncTargetRegistry.js'); const { _ } = require('lib/locale.js'); @@ -65,7 +65,7 @@ reg.scheduleSync = async (delay = null) => { const timeoutCallback = async () => { reg.scheduleSyncId_ = null; - reg.logger().info('Doing scheduled sync'); + reg.logger().info('Preparing scheduled sync'); const syncTargetId = Setting.value('sync.target'); @@ -82,6 +82,7 @@ reg.scheduleSync = async (delay = null) => { let context = Setting.value(contextKey); context = context ? JSON.parse(context) : {}; try { + reg.logger().info('Starting scheduled sync'); let newContext = await sync.start({ context: context }); Setting.setValue(contextKey, JSON.stringify(newContext)); } catch (error) { diff --git a/ReactNativeClient/lib/services/AlarmService.js b/ReactNativeClient/lib/services/AlarmService.js index 4817239ca..84349a547 100644 --- a/ReactNativeClient/lib/services/AlarmService.js +++ b/ReactNativeClient/lib/services/AlarmService.js @@ -1,4 +1,4 @@ -const { Note } = require('lib/models/note.js'); +const Note = require('lib/models/Note.js'); const Alarm = require('lib/models/Alarm.js'); class AlarmService { diff --git a/ReactNativeClient/lib/services/DecryptionWorker.js b/ReactNativeClient/lib/services/DecryptionWorker.js new file mode 100644 index 000000000..a275d90e9 --- /dev/null +++ b/ReactNativeClient/lib/services/DecryptionWorker.js @@ -0,0 +1,105 @@ +const BaseItem = require('lib/models/BaseItem'); +const { Logger } = require('lib/logger.js'); + +class DecryptionWorker { + + constructor() { + this.state_ = 'idle'; + this.logger_ = new Logger(); + + this.dispatch = (action) => { + //console.warn('DecryptionWorker.dispatch is not defined'); + }; + + this.scheduleId_ = null; + } + + setLogger(l) { + this.logger_ = l; + } + + logger() { + return this.logger_; + } + + static instance() { + if (this.instance_) return this.instance_; + this.instance_ = new DecryptionWorker(); + return this.instance_; + } + + setEncryptionService(v) { + this.encryptionService_ = v; + } + + encryptionService() { + if (!this.encryptionService_) throw new Error('DecryptionWorker.encryptionService_ is not set!!'); + return this.encryptionService_; + } + + async scheduleStart() { + if (this.scheduleId_) return; + + this.scheduleId_ = setTimeout(() => { + this.scheduleId_ = null; + this.start({ + materKeyNotLoadedHandler: 'dispatch', + }); + }, 1000); + } + + async start(options = null) { + if (options === null) options = {}; + if (!('materKeyNotLoadedHandler' in options)) options.materKeyNotLoadedHandler = 'throw'; + + if (this.state_ !== 'idle') { + this.logger().info('DecryptionWorker: cannot start because state is "' + this.state_ + '"'); + return; + } + + this.logger().info('DecryptionWorker: starting decryption...'); + + this.state_ = 'started'; + + let excludedIds = []; + + try { + while (true) { + const result = await BaseItem.itemsThatNeedDecryption(excludedIds); + const items = result.items; + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const ItemClass = BaseItem.itemClass(item); + this.logger().debug('DecryptionWorker: decrypting: ' + item.id + ' (' + ItemClass.tableName() + ')'); + try { + await ItemClass.decrypt(item); + } catch (error) { + if (error.code === 'masterKeyNotLoaded' && options.materKeyNotLoadedHandler === 'dispatch') { + excludedIds.push(item.id); + this.dispatch({ + type: 'MASTERKEY_ADD_NOT_LOADED', + id: error.masterKeyId, + }); + continue; + } + throw error; + } + } + + if (!result.hasMore) break; + } + } catch (error) { + this.logger().error('DecryptionWorker:', error); + this.state_ = 'idle'; + throw error; + } + + this.logger().info('DecryptionWorker: completed decryption.'); + + this.state_ = 'idle'; + } + +} + +module.exports = DecryptionWorker; \ No newline at end of file diff --git a/ReactNativeClient/lib/services/EncryptionService.js b/ReactNativeClient/lib/services/EncryptionService.js new file mode 100644 index 000000000..7600c394e --- /dev/null +++ b/ReactNativeClient/lib/services/EncryptionService.js @@ -0,0 +1,535 @@ +const { padLeft } = require('lib/string-utils.js'); +const { Logger } = require('lib/logger.js'); +const { shim } = require('lib/shim.js'); +const Setting = require('lib/models/Setting.js'); +const MasterKey = require('lib/models/MasterKey'); +const BaseItem = require('lib/models/BaseItem'); +const { _ } = require('lib/locale.js'); + +function hexPad(s, length) { + return padLeft(s, length, '0'); +} + +class EncryptionService { + + constructor() { + // Note: 1 MB is very slow with Node and probably even worse on mobile. 50 KB seems to work well + // and doesn't produce too much overhead in terms of headers. + this.chunkSize_ = 50000; + this.loadedMasterKeys_ = {}; + this.activeMasterKeyId_ = null; + this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL; + this.logger_ = new Logger(); + + this.headerTemplates_ = { + 1: { + fields: [ + [ 'encryptionMethod', 2, 'int' ], + [ 'masterKeyId', 32, 'hex' ], + ], + }, + }; + } + + static instance() { + if (this.instance_) return this.instance_; + this.instance_ = new EncryptionService(); + return this.instance_; + } + + setLogger(l) { + this.logger_ = l; + } + + logger() { + return this.logger_; + } + + async generateMasterKeyAndEnableEncryption(password) { + let masterKey = await this.generateMasterKey(password); + masterKey = await MasterKey.save(masterKey); + await this.enableEncryption(masterKey, password); + await this.loadMasterKeysFromSettings(); + return masterKey; + } + + async enableEncryption(masterKey, password = null) { + Setting.setValue('encryption.enabled', true); + Setting.setValue('encryption.activeMasterKeyId', masterKey.id); + + if (password) { + let passwordCache = Setting.value('encryption.passwordCache'); + passwordCache[masterKey.id] = password; + Setting.setValue('encryption.passwordCache', passwordCache); + } + + // Mark only the non-encrypted ones for sync since, if there are encrypted ones, + // it means they come from the sync target and are already encrypted over there. + await BaseItem.markAllNonEncryptedForSync(); + } + + async disableEncryption() { + // Allow disabling encryption even if some items are still encrypted, because whether E2EE is enabled or disabled + // should not affect whether items will enventually be decrypted or not (DecryptionWorker will still work as + // long as there are encrypted items). Also even if decryption is disabled, it's possible that encrypted items + // will still be received via synchronisation. + + // const hasEncryptedItems = await BaseItem.hasEncryptedItems(); + // if (hasEncryptedItems) throw new Error(_('Encryption cannot currently be disabled because some items are still encrypted. Please wait for all the items to be decrypted and try again.')); + + Setting.setValue('encryption.enabled', false); + // The only way to make sure everything gets decrypted on the sync target is + // to re-sync everything. + await BaseItem.forceSyncAll(); + } + + async loadMasterKeysFromSettings() { + const masterKeys = await MasterKey.all(); + const passwords = Setting.value('encryption.passwordCache'); + const activeMasterKeyId = Setting.value('encryption.activeMasterKeyId'); + + this.logger().info('Trying to load ' + masterKeys.length + ' master keys...'); + + for (let i = 0; i < masterKeys.length; i++) { + const mk = masterKeys[i]; + const password = passwords[mk.id]; + if (this.isMasterKeyLoaded(mk.id)) continue; + if (!password) continue; + + try { + await this.loadMasterKey(mk, password, activeMasterKeyId === mk.id); + } catch (error) { + this.logger().warn('Cannot load master key ' + mk.id + '. Invalid password?', error); + } + } + + this.logger().info('Loaded master keys: ' + this.loadedMasterKeysCount()); + } + + loadedMasterKeysCount() { + let output = 0; + for (let n in this.loadedMasterKeys_) { + if (!this.loadedMasterKeys_[n]) continue; + output++; + } + return output; + } + + chunkSize() { + return this.chunkSize_; + } + + defaultEncryptionMethod() { + return this.defaultEncryptionMethod_; + } + + setActiveMasterKeyId(id) { + this.activeMasterKeyId_ = id; + } + + activeMasterKeyId() { + if (!this.activeMasterKeyId_) { + const error = new Error('No master key is defined as active. Check this: Either one or more master keys exist but no password was provided for any of them. Or no master key exist. Or master keys and password exist, but none was set as active.'); + error.code = 'noActiveMasterKey'; + throw error; + } + return this.activeMasterKeyId_; + } + + isMasterKeyLoaded(id) { + return !!this.loadedMasterKeys_[id]; + } + + async loadMasterKey(model, password, makeActive = false) { + if (!model.id) throw new Error('Master key does not have an ID - save it first'); + this.loadedMasterKeys_[model.id] = await this.decryptMasterKey(model, password); + if (makeActive) this.setActiveMasterKeyId(model.id); + } + + unloadMasterKey(model) { + delete this.loadedMasterKeys_[model.id]; + } + + unloadAllMasterKeys() { + for (let id in this.loadedMasterKeys_) { + if (!this.loadedMasterKeys_.hasOwnProperty(id)) continue; + this.unloadMasterKey(this.loadedMasterKeys_[id]); + } + } + + loadedMasterKey(id) { + if (!this.loadedMasterKeys_[id]) { + const error = new Error('Master key is not loaded: ' + id); + error.code = 'masterKeyNotLoaded'; + error.masterKeyId = id; + throw error; + } + return this.loadedMasterKeys_[id]; + } + + loadedMasterKeyIds() { + let output = []; + for (let id in this.loadedMasterKeys_) { + if (!this.loadedMasterKeys_.hasOwnProperty(id)) continue; + output.push(id); + } + return output; + } + + fsDriver() { + if (!EncryptionService.fsDriver_) throw new Error('EncryptionService.fsDriver_ not set!'); + return EncryptionService.fsDriver_; + } + + sha256(string) { + const sjcl = shim.sjclModule; + const bitArray = sjcl.hash.sha256.hash(string); + return sjcl.codec.hex.fromBits(bitArray); + } + + async seedSjcl() { + throw new Error('NOT TESTED'); + + // Just putting this here in case it becomes needed + // Normally seeding random bytes is not needed for our use since + // we use shim.randomBytes directly to generate master keys. + + const sjcl = shim.sjclModule; + const randomBytes = await shim.randomBytes(1024/8); + const hexBytes = randomBytes.map((a) => { return a.toString(16) }); + const hexSeed = sjcl.codec.hex.toBits(hexBytes.join('')); + sjcl.random.addEntropy(hexSeed, 1024, 'shim.randomBytes'); + } + + async generateMasterKey(password) { + const bytes = await shim.randomBytes(256); + const hexaBytes = bytes.map((a) => { return hexPad(a.toString(16), 2); }).join(''); + const checksum = this.sha256(hexaBytes); + const encryptionMethod = EncryptionService.METHOD_SJCL_2; + const cipherText = await this.encrypt(encryptionMethod, password, hexaBytes); + const now = Date.now(); + + return { + created_time: now, + updated_time: now, + source_application: Setting.value('appId'), + encryption_method: encryptionMethod, + checksum: checksum, + content: cipherText, + }; + } + + async decryptMasterKey(model, password) { + const plainText = await this.decrypt(model.encryption_method, password, model.content); + const checksum = this.sha256(plainText); + if (checksum !== model.checksum) throw new Error('Could not decrypt master key (checksum failed)'); + return plainText; + } + + async checkMasterKeyPassword(model, password) { + try { + await this.decryptMasterKey(model, password); + } catch (error) { + return false; + } + + return true; + } + + async encrypt(method, key, plainText) { + if (!method) throw new Error('Encryption method is required'); + if (!key) throw new Error('Encryption key is required'); + + const sjcl = shim.sjclModule; + + if (method === EncryptionService.METHOD_SJCL) { + try { + // Good demo to understand each parameter: https://bitwiseshiftleft.github.io/sjcl/demo/ + return sjcl.json.encrypt(key, plainText, { + v: 1, // version + iter: 1000, // Defaults to 10000 in sjcl but since we're running this on mobile devices, use a lower value. Maybe review this after some time. https://security.stackexchange.com/questions/3959/recommended-of-iterations-when-using-pkbdf2-sha256 + ks: 128, // Key size - "128 bits should be secure enough" + ts: 64, // ??? + mode: "ocb2", // The cipher mode is a standard for how to use AES and other algorithms to encrypt and authenticate your message. OCB2 mode is slightly faster and has more features, but CCM mode has wider support because it is not patented. + //"adata":"", // Associated Data - not needed? + cipher: "aes" + }); + } catch (error) { + // SJCL returns a string as error which means stack trace is missing so convert to an error object here + throw new Error(error.message); + } + } + + // Same as first one but slightly more secure (but slower) to encrypt master keys + if (method === EncryptionService.METHOD_SJCL_2) { + try { + return sjcl.json.encrypt(key, plainText, { + v: 1, + iter: 10000, + ks: 256, + ts: 64, + mode: "ocb2", + cipher: "aes" + }); + } catch (error) { + // SJCL returns a string as error which means stack trace is missing so convert to an error object here + throw new Error(error.message); + } + } + + throw new Error('Unknown encryption method: ' + method); + } + + async decrypt(method, key, cipherText) { + if (!method) throw new Error('Encryption method is required'); + if (!key) throw new Error('Encryption key is required'); + + const sjcl = shim.sjclModule; + + if (method === EncryptionService.METHOD_SJCL || method === EncryptionService.METHOD_SJCL_2) { + try { + return sjcl.json.decrypt(key, cipherText); + } catch (error) { + // SJCL returns a string as error which means stack trace is missing so convert to an error object here + throw new Error(error.message); + } + } + + throw new Error('Unknown decryption method: ' + method); + } + + async encryptAbstract_(source, destination) { + const method = this.defaultEncryptionMethod(); + const masterKeyId = this.activeMasterKeyId(); + const masterKeyPlainText = this.loadedMasterKey(masterKeyId); + + const header = { + encryptionMethod: method, + masterKeyId: masterKeyId, + }; + + await destination.append(this.encodeHeader_(header)); + + while (true) { + const block = await source.read(this.chunkSize_); + if (!block) break; + + const encrypted = await this.encrypt(method, masterKeyPlainText, block); + await destination.append(padLeft(encrypted.length.toString(16), 6, '0')); + await destination.append(encrypted); + } + } + + async decryptAbstract_(source, destination) { + const identifier = await source.read(5); + if (!this.isValidHeaderIdentifier(identifier)) throw new Error('Invalid encryption identifier. Data is not actually encrypted? ID was: ' + identifier); + const mdSizeHex = await source.read(6); + const mdSize = parseInt(mdSizeHex, 16); + if (isNaN(mdSize) || !mdSize) throw new Error('Invalid header metadata size: ' + mdSizeHex); + const md = await source.read(parseInt(mdSizeHex, 16)); + const header = this.decodeHeader_(identifier + mdSizeHex + md); + const masterKeyPlainText = this.loadedMasterKey(header.masterKeyId); + + while (true) { + const lengthHex = await source.read(6); + if (!lengthHex) break; + if (lengthHex.length !== 6) throw new Error('Invalid block size: ' + lengthHex); + const length = parseInt(lengthHex, 16); + if (!length) continue; // Weird but could be not completely invalid (block of size 0) so continue decrypting + + const block = await source.read(length); + + const plainText = await this.decrypt(header.encryptionMethod, masterKeyPlainText, block); + await destination.append(plainText); + } + } + + stringReader_(string, sync = false) { + const reader = { + index: 0, + read: function(size) { + const output = string.substr(reader.index, size); + reader.index += size; + return !sync ? Promise.resolve(output) : output; + }, + close: function() {}, + }; + return reader; + } + + stringWriter_() { + const output = { + data: [], + append: async function(data) { + output.data.push(data); + }, + result: function() { + return output.data.join(''); + }, + close: function() {}, + }; + return output; + } + + async fileReader_(path, encoding) { + const handle = await this.fsDriver().open(path, 'r'); + const reader = { + handle: handle, + read: async (size) => { + return this.fsDriver().readFileChunk(reader.handle, size, encoding); + }, + close: () => { + this.fsDriver().close(reader.handle); + }, + }; + return reader; + } + + async fileWriter_(path, encoding) { + return { + append: async (data) => { + return this.fsDriver().appendFile(path, data, encoding); + }, + close: function() {}, + }; + } + + async encryptString(plainText) { + const source = this.stringReader_(plainText); + const destination = this.stringWriter_(); + await this.encryptAbstract_(source, destination); + return destination.result(); + } + + async decryptString(cipherText) { + const source = this.stringReader_(cipherText); + const destination = this.stringWriter_(); + await this.decryptAbstract_(source, destination); + return destination.data.join(''); + } + + async encryptFile(srcPath, destPath) { + let source = await this.fileReader_(srcPath, 'base64'); + let destination = await this.fileWriter_(destPath, 'ascii'); + + const cleanUp = () => { + if (source) source.close(); + if (destination) destination.close(); + source = null; + destination = null; + } + + try { + await this.fsDriver().unlink(destPath); + await this.encryptAbstract_(source, destination); + } catch (error) { + cleanUp(); + await this.fsDriver().unlink(destPath); + throw error; + } + + cleanUp(); + } + + async decryptFile(srcPath, destPath) { + let source = await this.fileReader_(srcPath, 'ascii'); + let destination = await this.fileWriter_(destPath, 'base64'); + + const cleanUp = () => { + if (source) source.close(); + if (destination) destination.close(); + source = null; + destination = null; + } + + try { + await this.fsDriver().unlink(destPath); + await this.decryptAbstract_(source, destination); + } catch (error) { + cleanUp(); + await this.fsDriver().unlink(destPath); + throw error; + } + + cleanUp(); + } + + decodeHeaderVersion_(hexaByte) { + if (hexaByte.length !== 2) throw new Error('Invalid header version length: ' + hexaByte); + return parseInt(hexaByte, 16); + } + + headerTemplate(version) { + const r = this.headerTemplates_[version]; + if (!r) throw new Error('Unknown header version: ' + version); + return r; + } + + encodeHeader_(header) { + // Sanity check + if (header.masterKeyId.length !== 32) throw new Error('Invalid master key ID size: ' + header.masterKeyId); + + let encryptionMetadata = ''; + encryptionMetadata += padLeft(header.encryptionMethod.toString(16), 2, '0'); + encryptionMetadata += header.masterKeyId; + encryptionMetadata = padLeft(encryptionMetadata.length.toString(16), 6, '0') + encryptionMetadata; + return 'JED01' + encryptionMetadata; + } + + decodeHeader_(headerHexaBytes) { + const reader = this.stringReader_(headerHexaBytes, true); + const identifier = reader.read(3); + const version = parseInt(reader.read(2), 16); + if (identifier !== 'JED') throw new Error('Invalid header (missing identifier): ' + headerHexaBytes.substr(0,64)); + const template = this.headerTemplate(version); + + const size = parseInt(reader.read(6), 16); + + let output = {}; + + for (let i = 0; i < template.fields.length; i++) { + const m = template.fields[i]; + const type = m[2]; + let v = reader.read(m[1]); + + if (type === 'int') { + v = parseInt(v, 16); + } else if (type === 'hex') { + // Already in hexa + } else { + throw new Error('Invalid type: ' + type); + } + + output[m[0]] = v; + } + + return output; + } + + isValidHeaderIdentifier(id, ignoreTooLongLength = false) { + if (!ignoreTooLongLength && !id || id.length !== 5) return false; + return /JED\d\d/.test(id); + } + + async itemIsEncrypted(item) { + if (!item) throw new Error('No item'); + const ItemClass = BaseItem.itemClass(item); + if (!ItemClass.encryptionSupported()) return false; + return item.encryption_applied && this.isValidHeaderIdentifier(item.encryption_cipher_text); + } + + async fileIsEncrypted(path) { + const handle = await this.fsDriver().open(path, 'r'); + const headerIdentifier = await this.fsDriver().readFileChunk(handle, 5, 'ascii'); + await this.fsDriver().close(handle); + return this.isValidHeaderIdentifier(headerIdentifier); + } + +} + +EncryptionService.METHOD_SJCL = 1; +EncryptionService.METHOD_SJCL_2 = 2; + +EncryptionService.fsDriver_ = null; + +module.exports = EncryptionService; \ No newline at end of file diff --git a/ReactNativeClient/lib/services/EncryptionServiceDriverNode.js b/ReactNativeClient/lib/services/EncryptionServiceDriverNode.js new file mode 100644 index 000000000..e69de29bb diff --git a/ReactNativeClient/lib/services/EncryptionServiceDriverRN.js b/ReactNativeClient/lib/services/EncryptionServiceDriverRN.js new file mode 100644 index 000000000..e704c22d5 --- /dev/null +++ b/ReactNativeClient/lib/services/EncryptionServiceDriverRN.js @@ -0,0 +1,9 @@ +class EncryptionServiceDriverRN { + + encryptFile(srcPath, destPath) { + + } + +} + +module.exports = EncryptionServiceDriverRN; \ No newline at end of file diff --git a/ReactNativeClient/lib/services/exporter.js b/ReactNativeClient/lib/services/exporter.js index fc33b1d59..65044be5a 100644 --- a/ReactNativeClient/lib/services/exporter.js +++ b/ReactNativeClient/lib/services/exporter.js @@ -1,10 +1,10 @@ -const { BaseItem } = require('lib/models/base-item.js'); -const { BaseModel } = require('lib/base-model.js'); -const { Resource } = require('lib/models/resource.js'); -const { Folder } = require('lib/models/folder.js'); -const { NoteTag } = require('lib/models/note-tag.js'); -const { Note } = require('lib/models/note.js'); -const { Tag } = require('lib/models/tag.js'); +const BaseItem = require('lib/models/BaseItem.js'); +const BaseModel = require('lib/BaseModel.js'); +const Resource = require('lib/models/Resource.js'); +const Folder = require('lib/models/Folder.js'); +const NoteTag = require('lib/models/NoteTag.js'); +const Note = require('lib/models/Note.js'); +const Tag = require('lib/models/Tag.js'); const { basename } = require('lib/path-utils.js'); const fs = require('fs-extra'); diff --git a/ReactNativeClient/lib/services/report.js b/ReactNativeClient/lib/services/report.js index 815ec2129..8bbb227b2 100644 --- a/ReactNativeClient/lib/services/report.js +++ b/ReactNativeClient/lib/services/report.js @@ -1,8 +1,8 @@ const { time } = require('lib/time-utils'); -const { BaseItem } = require('lib/models/base-item.js'); +const BaseItem = require('lib/models/BaseItem.js'); const Alarm = require('lib/models/Alarm'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); const { _ } = require('lib/locale.js'); class ReportService { @@ -118,8 +118,12 @@ class ReportService { for (let i = 0; i < disabledItems.length; i++) { const row = disabledItems[i]; - section.body.push(_('"%s": "%s"', row.item.title, row.syncInfo.sync_disabled_reason)); + section.body.push(_('%s (%s): %s', row.item.title, row.item.id, row.syncInfo.sync_disabled_reason)); } + + section.body.push(''); + section.body.push(_('These items will remain on the device but will not be uploaded to the sync target. In order to find these items, either search for the title or the ID (which is displayed in brackets above).')); + sections.push(section); } diff --git a/ReactNativeClient/lib/shim-init-node.js b/ReactNativeClient/lib/shim-init-node.js index 7007375e4..04861d594 100644 --- a/ReactNativeClient/lib/shim-init-node.js +++ b/ReactNativeClient/lib/shim-init-node.js @@ -10,6 +10,12 @@ function shimInit() { shim.FileApiDriverLocal = FileApiDriverLocal; shim.Geolocation = GeolocationNode; shim.FormData = require('form-data'); + shim.sjclModule = require('lib/vendor/sjcl.js'); + + shim.randomBytes = async (count) => { + const buffer = require('crypto').randomBytes(count); + return Array.from(buffer); + } shim.detectAndSetLocale = function (Setting) { let locale = process.env.LANG; @@ -24,7 +30,7 @@ function shimInit() { const resizeImage_ = async function(filePath, targetPath) { const sharp = require('sharp'); - const { Resource } = require('lib/models/resource.js'); + const Resource = require('lib/models/Resource.js'); return new Promise((resolve, reject) => { sharp(filePath) @@ -42,11 +48,11 @@ function shimInit() { } shim.attachFileToNote = async function(note, filePath) { - const { Resource } = require('lib/models/resource.js'); + const Resource = require('lib/models/Resource.js'); const { uuid } = require('lib/uuid.js'); const { basename, fileExtension, safeFileExtension } = require('lib/path-utils.js'); const mime = require('mime/lite'); - const { Note } = require('lib/models/note.js'); + const Note = require('lib/models/Note.js'); if (!(await fs.pathExists(filePath))) throw new Error(_('Cannot access %s', filePath)); @@ -150,6 +156,16 @@ function shimInit() { return shim.fetchWithRetry(doFetchOperation, options); } + + shim.uploadBlob = async function(url, options) { + if (!options || !options.path) throw new Error('uploadBlob: source file path is missing'); + const content = await fs.readFile(options.path); + options = Object.assign({}, options, { + body: content, + }); + return shim.fetch(url, options); + } + } module.exports = { shimInit }; \ No newline at end of file diff --git a/ReactNativeClient/lib/shim-init-react.js b/ReactNativeClient/lib/shim-init-react.js index d690d2c48..c2f9590cd 100644 --- a/ReactNativeClient/lib/shim-init-react.js +++ b/ReactNativeClient/lib/shim-init-react.js @@ -2,12 +2,23 @@ const { shim } = require('lib/shim.js'); const { GeolocationReact } = require('lib/geolocation-react.js'); const { PoorManIntervals } = require('lib/poor-man-intervals.js'); const RNFetchBlob = require('react-native-fetch-blob').default; +const { generateSecureRandom } = require('react-native-securerandom'); function shimInit() { shim.Geolocation = GeolocationReact; - shim.setInterval = PoorManIntervals.setInterval; shim.clearInterval = PoorManIntervals.clearInterval; + shim.sjclModule = require('lib/vendor/sjcl-rn.js'); + + shim.randomBytes = async (count) => { + const randomBytes = await generateSecureRandom(count); + let temp = []; + for (let n in randomBytes) { + if (!randomBytes.hasOwnProperty(n)) continue; + temp.push(randomBytes[n]); + } + return temp; + } shim.fetch = async function(url, options = null) { return shim.fetchWithRetry(() => { @@ -35,10 +46,7 @@ function shimInit() { try { const response = await shim.fetchWithRetry(doFetchBlob, options); - // let response = await RNFetchBlob.config({ - // path: localFilePath - // }).fetch(method, url, headers); - + // Returns an object that's roughtly compatible with a standard Response object let output = { ok: response.respInfo.status < 400, diff --git a/ReactNativeClient/lib/shim.js b/ReactNativeClient/lib/shim.js index 322d0acf7..931763911 100644 --- a/ReactNativeClient/lib/shim.js +++ b/ReactNativeClient/lib/shim.js @@ -111,6 +111,8 @@ shim.fs = null; shim.FileApiDriverLocal = null; shim.readLocalFileBase64 = (path) => { throw new Error('Not implemented'); } shim.uploadBlob = () => { throw new Error('Not implemented'); } +shim.sjclModule = null; +shim.randomBytes = async (count) => { throw new Error('Not implemented'); } shim.setInterval = function(fn, interval) { return setInterval(fn, interval); } diff --git a/ReactNativeClient/lib/string-utils.js b/ReactNativeClient/lib/string-utils.js index b794063bf..94e5ff3ff 100644 --- a/ReactNativeClient/lib/string-utils.js +++ b/ReactNativeClient/lib/string-utils.js @@ -191,4 +191,14 @@ function splitCommandString(command) { return args; } -module.exports = { removeDiacritics, escapeFilename, wrap, splitCommandString }; \ No newline at end of file +function padLeft(string, length, padString) { + if (!string) return ''; + + while (string.length < length) { + string = padString + string; + } + + return string; +} + +module.exports = { removeDiacritics, escapeFilename, wrap, splitCommandString, padLeft }; \ No newline at end of file diff --git a/ReactNativeClient/lib/synchronizer.js b/ReactNativeClient/lib/synchronizer.js index 2faf767e3..3281efa94 100644 --- a/ReactNativeClient/lib/synchronizer.js +++ b/ReactNativeClient/lib/synchronizer.js @@ -1,8 +1,10 @@ -const { BaseItem } = require('lib/models/base-item.js'); -const { Folder } = require('lib/models/folder.js'); -const { Note } = require('lib/models/note.js'); -const { Resource } = require('lib/models/resource.js'); -const { BaseModel } = require('lib/base-model.js'); +const BaseItem = require('lib/models/BaseItem.js'); +const Folder = require('lib/models/Folder.js'); +const Note = require('lib/models/Note.js'); +const Resource = require('lib/models/Resource.js'); +const MasterKey = require('lib/models/MasterKey.js'); +const BaseModel = require('lib/BaseModel.js'); +const DecryptionWorker = require('lib/services/DecryptionWorker'); const { sprintf } = require('sprintf-js'); const { time } = require('lib/time-utils.js'); const { Logger } = require('lib/logger.js'); @@ -21,6 +23,7 @@ class Synchronizer { this.logger_ = new Logger(); this.appType_ = appType; this.cancelling_ = false; + this.autoStartDecryptionWorker_ = true; // Debug flags are used to test certain hard-to-test conditions // such as cancelling in the middle of a loop. @@ -52,6 +55,14 @@ class Synchronizer { return this.logger_; } + setEncryptionService(v) { + this.encryptionService_ = v; + } + + encryptionService(v) { + return this.encryptionService_; + } + static reportToLines(report) { let lines = []; if (report.createLocal) lines.push(_('Created local items: %d.', report.createLocal)); @@ -169,6 +180,9 @@ class Synchronizer { this.cancelling_ = false; + const masterKeysBefore = await MasterKey.count(); + let hasAutoEnabledEncryption = false; + // ------------------------------------------------------------------------ // First, find all the items that have been changed since the // last sync and apply the changes to remote. @@ -177,7 +191,7 @@ class Synchronizer { let synchronizationId = time.unixMs().toString(); let outputContext = Object.assign({}, lastContext); - + this.dispatch({ type: 'SYNC_STARTED' }); this.logSyncOperation('starting', null, null, 'Starting synchronisation to target ' + syncTargetId + '... [' + synchronizationId + ']'); @@ -200,11 +214,12 @@ class Synchronizer { let ItemClass = BaseItem.itemClass(local); let path = BaseItem.systemPath(local); - // Safety check to avoid infinite loops: + // Safety check to avoid infinite loops. + // In fact this error is possible if the item is marked for sync (via sync_time or force_sync) while synchronisation is in + // progress. In that case exit anyway to be sure we aren't in a loop and the item will be re-synced next time. if (donePaths.indexOf(path) > 0) throw new Error(sprintf('Processing a path that has already been done: %s. sync_time was not updated?', path)); let remote = await this.api().stat(path); - let content = await ItemClass.serialize(local); let action = null; let updateSyncTimeOnly = true; let reason = ''; @@ -258,26 +273,15 @@ class Synchronizer { this.dispatch({ type: 'SYNC_HAS_DISABLED_SYNC_ITEMS' }); } - if (local.type_ == BaseModel.TYPE_RESOURCE && (action == 'createRemote' || (action == 'itemConflict' && remote))) { - let remoteContentPath = this.resourceDirName_ + '/' + local.id; + if (local.type_ == BaseModel.TYPE_RESOURCE && (action == 'createRemote' || action === 'updateRemote' || (action == 'itemConflict' && remote))) { try { - // TODO: handle node and mobile in the same way - if (shim.isNode()) { - let resourceContent = ''; - try { - resourceContent = await Resource.content(local); - } catch (error) { - error.message = 'Cannot read resource content: ' + local.id + ': ' + error.message; - this.logger().error(error); - this.progressReport_.errors.push(error); - } - await this.api().put(remoteContentPath, resourceContent); - } else { - const localResourceContentPath = Resource.fullPath(local); - await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' }); - } + const remoteContentPath = this.resourceDirName_ + '/' + local.id; + const result = await Resource.fullPathForSyncUpload(local); + local = result.resource; + const localResourceContentPath = result.path; + await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' }); } catch (error) { - if (error && error.code === 'cannotSync') { + if (error && error.code === 'rejectedByTarget') { await handleCannotSyncItem(syncTargetId, local, error.message); action = null; } else { @@ -301,14 +305,15 @@ class Synchronizer { let canSync = true; try { - if (this.debugFlags_.indexOf('cannotSync') >= 0) { - const error = new Error('Testing cannotSync'); - error.code = 'cannotSync'; + if (this.debugFlags_.indexOf('rejectedByTarget') >= 0) { + const error = new Error('Testing rejectedByTarget'); + error.code = 'rejectedByTarget'; throw error; } + const content = await ItemClass.serializeForSync(local); await this.api().put(path, content); } catch (error) { - if (error && error.code === 'cannotSync') { + if (error && error.code === 'rejectedByTarget') { await handleCannotSyncItem(syncTargetId, local, error.message); canSync = false; } else { @@ -323,6 +328,11 @@ class Synchronizer { } else if (action == 'itemConflict') { + // ------------------------------------------------------------------------------ + // For non-note conflicts, we take the remote version (i.e. the version that was + // synced first) and overwrite the local content. + // ------------------------------------------------------------------------------ + if (remote) { local = remoteContent; @@ -501,6 +511,17 @@ class Synchronizer { await ItemClass.save(content, options); + if (!hasAutoEnabledEncryption && content.type_ === BaseModel.TYPE_MASTER_KEY && !masterKeysBefore) { + hasAutoEnabledEncryption = true; + this.logger().info('One master key was downloaded and none was previously available: automatically enabling encryption'); + this.logger().info('Using master key: ', content); + await this.encryptionService().enableEncryption(content); + await this.encryptionService().loadMasterKeysFromSettings(); + this.logger().info('Encryption has been enabled with downloaded master key as active key. However, note that no password was initially supplied. It will need to be provided by user.'); + } + + if (!!content.encryption_applied) this.dispatch({ type: 'SYNC_GOT_ENCRYPTED_ITEM' }); + } else if (action == 'deleteLocal') { if (local.type_ == BaseModel.TYPE_FOLDER) { @@ -553,8 +574,14 @@ class Synchronizer { await BaseItem.deleteOrphanSyncItems(); } } catch (error) { - this.logger().error(error); - this.progressReport_.errors.push(error); + if (error && ['cannotEncryptEncrypted', 'noActiveMasterKey'].indexOf(error.code) >= 0) { + // Only log an info statement for this since this is a common condition that is reported + // in the application, and needs to be resolved by the user + this.logger().info(error.message); + } else { + this.logger().error(error); + this.progressReport_.errors.push(error); + } } if (this.cancelling()) { diff --git a/ReactNativeClient/lib/vendor/sjcl-rn.js b/ReactNativeClient/lib/vendor/sjcl-rn.js new file mode 100644 index 000000000..3c5dee0e5 --- /dev/null +++ b/ReactNativeClient/lib/vendor/sjcl-rn.js @@ -0,0 +1,70 @@ +// https://github.com/bitwiseshiftleft/sjcl +// Commit: 4fc74ff92fd6b836cc596cc0ee940ef6cc8db7c6 +// +// NOTE: Need a special version of this lib for React Native. The following patch has been applied to the below code: +// +// - try{H=require('crypto')}catch(a){H=null} +// + try{H=null}catch(a){H=null} +// +// This is because crypto is not part of React Native and that code will throw an exception. +// The crypto module is apparently used only for random number generation - https://github.com/pouchdb/pouchdb/issues/3787 +"use strict";var sjcl={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}}; +sjcl.cipher.aes=function(a){this.s[0][0][0]||this.O();var b,c,d,e,f=this.s[0][4],g=this.s[1];b=a.length;var h=1;if(4!==b&&6!==b&&8!==b)throw new sjcl.exception.invalid("invalid aes key size");this.b=[d=a.slice(0),e=[]];for(a=b;a<4*b+28;a++){c=d[a-1];if(0===a%b||8===b&&4===a%b)c=f[c>>>24]<<24^f[c>>16&255]<<16^f[c>>8&255]<<8^f[c&255],0===a%b&&(c=c<<8^c>>>24^h<<24,h=h<<1^283*(h>>7));d[a]=d[a-b]^c}for(b=0;a;b++,a--)c=d[b&3?a:a-4],e[b]=4>=a||4>b?c:g[0][f[c>>>24]]^g[1][f[c>>16&255]]^g[2][f[c>>8&255]]^g[3][f[c& +255]]}; +sjcl.cipher.aes.prototype={encrypt:function(a){return t(this,a,0)},decrypt:function(a){return t(this,a,1)},s:[[[],[],[],[],[]],[[],[],[],[],[]]],O:function(){var a=this.s[0],b=this.s[1],c=a[4],d=b[4],e,f,g,h=[],k=[],l,n,m,p;for(e=0;0x100>e;e++)k[(h[e]=e<<1^283*(e>>7))^e]=e;for(f=g=0;!c[f];f^=l||1,g=k[g]||1)for(m=g^g<<1^g<<2^g<<3^g<<4,m=m>>8^m&255^99,c[f]=m,d[m]=f,n=h[e=h[l=h[f]]],p=0x1010101*n^0x10001*e^0x101*l^0x1010100*f,n=0x101*h[m]^0x1010100*m,e=0;4>e;e++)a[e][f]=n=n<<24^n>>>8,b[e][m]=p=p<<24^p>>>8;for(e= +0;5>e;e++)a[e]=a[e].slice(0),b[e]=b[e].slice(0)}}; +function t(a,b,c){if(4!==b.length)throw new sjcl.exception.invalid("invalid aes block size");var d=a.b[c],e=b[0]^d[0],f=b[c?3:1]^d[1],g=b[2]^d[2];b=b[c?1:3]^d[3];var h,k,l,n=d.length/4-2,m,p=4,r=[0,0,0,0];h=a.s[c];a=h[0];var q=h[1],v=h[2],w=h[3],x=h[4];for(m=0;m>>24]^q[f>>16&255]^v[g>>8&255]^w[b&255]^d[p],k=a[f>>>24]^q[g>>16&255]^v[b>>8&255]^w[e&255]^d[p+1],l=a[g>>>24]^q[b>>16&255]^v[e>>8&255]^w[f&255]^d[p+2],b=a[b>>>24]^q[e>>16&255]^v[f>>8&255]^w[g&255]^d[p+3],p+=4,e=h,f=k,g=l;for(m= +0;4>m;m++)r[c?3&-m:m]=x[e>>>24]<<24^x[f>>16&255]<<16^x[g>>8&255]<<8^x[b&255]^d[p++],h=e,e=f,f=g,g=b,b=h;return r} +sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.$(a.slice(b/32),32-(b&31)).slice(1);return void 0===c?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var d=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-d^a[b/32+1|0]>>>d:a[b/32|0]>>>d)&(1<>b-1,1));return a},partial:function(a,b,c){return 32===a?b:(c?b|0:b<<32-a)+0x10000000000*a},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return!1;var c=0,d;for(d=0;d>>b),c=a[e]<<32-b;e=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(e);d.push(sjcl.bitArray.partial(b+a&31,32>>24|c>>>8&0xff00|(c&0xff00)<<8|c<<24;return a}}; +sjcl.codec.utf8String={fromBits:function(a){var b="",c=sjcl.bitArray.bitLength(a),d,e;for(d=0;d>>8>>>8>>>8),e<<=8;return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,d=0;for(c=0;c>>g)>>>e),gn){if(!b)try{return sjcl.codec.base32hex.toBits(a)}catch(p){}throw new sjcl.exception.invalid("this isn't "+m+"!");}h>e?(h-=e,f.push(l^n>>>h),l=n<>>e)>>>26),6>e?(g=a[c]<<6-e,e+=26,c++):(g<<=6,e-=6);for(;d.length&3&&!b;)d+="=";return d},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],d,e=0,f=sjcl.codec.base64.B,g=0,h;b&&(f=f.substr(0,62)+"-_");for(d=0;dh)throw new sjcl.exception.invalid("this isn't base64!");26>>e),g=h<<32-e):(e+=6,g^=h<<32-e)}e&56&&c.push(sjcl.bitArray.partial(e&56,g,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.b[0]||this.O();a?(this.F=a.F.slice(0),this.A=a.A.slice(0),this.l=a.l):this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()}; +sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.F=this.Y.slice(0);this.A=[];this.l=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.A=sjcl.bitArray.concat(this.A,a);b=this.l;a=this.l=b+sjcl.bitArray.bitLength(a);if(0x1fffffffffffffb;c++){e=!0;for(d=2;d*d<=c;d++)if(0===c%d){e= +!1;break}e&&(8>b&&(this.Y[b]=a(Math.pow(c,.5))),this.b[b]=a(Math.pow(c,1/3)),b++)}}}; +function u(a,b){var c,d,e,f=a.F,g=a.b,h=f[0],k=f[1],l=f[2],n=f[3],m=f[4],p=f[5],r=f[6],q=f[7];for(c=0;64>c;c++)16>c?d=b[c]:(d=b[c+1&15],e=b[c+14&15],d=b[c&15]=(d>>>7^d>>>18^d>>>3^d<<25^d<<14)+(e>>>17^e>>>19^e>>>10^e<<15^e<<13)+b[c&15]+b[c+9&15]|0),d=d+q+(m>>>6^m>>>11^m>>>25^m<<26^m<<21^m<<7)+(r^m&(p^r))+g[c],q=r,r=p,p=m,m=n+d|0,n=l,l=k,k=h,h=d+(k&l^n&(k^l))+(k>>>2^k>>>13^k>>>22^k<<30^k<<19^k<<10)|0;f[0]=f[0]+h|0;f[1]=f[1]+k|0;f[2]=f[2]+l|0;f[3]=f[3]+n|0;f[4]=f[4]+m|0;f[5]=f[5]+p|0;f[6]=f[6]+r|0;f[7]= +f[7]+q|0} +sjcl.mode.ccm={name:"ccm",G:[],listenProgress:function(a){sjcl.mode.ccm.G.push(a)},unListenProgress:function(a){a=sjcl.mode.ccm.G.indexOf(a);-1k)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(f=2;4>f&&l>>>8*f;f++);f<15-k&&(f=15-k);c=h.clamp(c, +8*(15-f));b=sjcl.mode.ccm.V(a,b,c,d,e,f);g=sjcl.mode.ccm.C(a,g,c,b,e,f);return h.concat(g.data,g.tag)},decrypt:function(a,b,c,d,e){e=e||64;d=d||[];var f=sjcl.bitArray,g=f.bitLength(c)/8,h=f.bitLength(b),k=f.clamp(b,h-e),l=f.bitSlice(b,h-e),h=(h-e)/8;if(7>g)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(b=2;4>b&&h>>>8*b;b++);b<15-g&&(b=15-g);c=f.clamp(c,8*(15-b));k=sjcl.mode.ccm.C(a,k,c,l,e,b);a=sjcl.mode.ccm.V(a,k.data,c,d,e,b);if(!f.equal(k.tag,a))throw new sjcl.exception.corrupt("ccm: tag doesn't match"); +return k.data},na:function(a,b,c,d,e,f){var g=[],h=sjcl.bitArray,k=h.i;d=[h.partial(8,(b.length?64:0)|d-2<<2|f-1)];d=h.concat(d,c);d[3]|=e;d=a.encrypt(d);if(b.length)for(c=h.bitLength(b)/8,65279>=c?g=[h.partial(16,c)]:0xffffffff>=c&&(g=h.concat([h.partial(16,65534)],[c])),g=h.concat(g,b),b=0;be||16n&&(sjcl.mode.ccm.fa(g/ +k),n+=m),c[3]++,e=a.encrypt(c),b[g]^=e[0],b[g+1]^=e[1],b[g+2]^=e[2],b[g+3]^=e[3];return{tag:d,data:h.clamp(b,l)}}}; +sjcl.mode.ocb2={name:"ocb2",encrypt:function(a,b,c,d,e,f){if(128!==sjcl.bitArray.bitLength(c))throw new sjcl.exception.invalid("ocb iv must be 128 bits");var g,h=sjcl.mode.ocb2.S,k=sjcl.bitArray,l=k.i,n=[0,0,0,0];c=h(a.encrypt(c));var m,p=[];d=d||[];e=e||64;for(g=0;g+4e.bitLength(c)&&(h=f(h,d(h)),c=e.concat(c,[-2147483648,0,0,0]));g=f(g,c); +return a.encrypt(f(d(f(h,d(h))),g))},S:function(a){return[a[0]<<1^a[1]>>>31,a[1]<<1^a[2]>>>31,a[2]<<1^a[3]>>>31,a[3]<<1^135*(a[0]>>>31)]}}; +sjcl.mode.gcm={name:"gcm",encrypt:function(a,b,c,d,e){var f=b.slice(0);b=sjcl.bitArray;d=d||[];a=sjcl.mode.gcm.C(!0,a,f,d,c,e||128);return b.concat(a.data,a.tag)},decrypt:function(a,b,c,d,e){var f=b.slice(0),g=sjcl.bitArray,h=g.bitLength(f);e=e||128;d=d||[];e<=h?(b=g.bitSlice(f,h-e),f=g.bitSlice(f,0,h-e)):(b=f,f=[]);a=sjcl.mode.gcm.C(!1,a,f,d,c,e);if(!g.equal(a.tag,b))throw new sjcl.exception.corrupt("gcm: tag doesn't match");return a.data},ka:function(a,b){var c,d,e,f,g,h=sjcl.bitArray.i;e=[0,0, +0,0];f=b.slice(0);for(c=0;128>c;c++){(d=0!==(a[Math.floor(c/32)]&1<<31-c%32))&&(e=h(e,f));g=0!==(f[3]&1);for(d=3;0>>1|(f[d-1]&1)<<31;f[0]>>>=1;g&&(f[0]^=-0x1f000000)}return e},j:function(a,b,c){var d,e=c.length;b=b.slice(0);for(d=0;de&&(a=b.hash(a));for(d=0;dd||0>c)throw new sjcl.exception.invalid("invalid params to pbkdf2");"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));"string"===typeof b&&(b=sjcl.codec.utf8String.toBits(b));e=e||sjcl.misc.hmac;a=new e(a);var f,g,h,k,l=[],n=sjcl.bitArray;for(k=1;32*l.length<(d||1);k++){e=f=a.encrypt(n.concat(b,[k]));for(g=1;gg;g++)e.push(0x100000000*Math.random()|0);for(g=0;g=1<this.o&&(this.o= +f);this.P++;this.b=sjcl.hash.sha256.hash(this.b.concat(e));this.L=new sjcl.cipher.aes(this.b);for(d=0;4>d&&(this.h[d]=this.h[d]+1|0,!this.h[d]);d++);}for(d=0;d>>1;this.c[g].update([d,this.N++,2,b,f,a.length].concat(a))}break;case "string":void 0===b&&(b=a.length);this.c[g].update([d,this.N++,3,b,f,a.length]);this.c[g].update(a);break;default:k=1}if(k)throw new sjcl.exception.bug("random: addEntropy only supports number, array of numbers or string");this.m[g]+=b;this.f+=b;h===this.u&&(this.isReady()!==this.u&&A("seeded",Math.max(this.o,this.f)),A("progress",this.getProgress()))}, +isReady:function(a){a=this.T[void 0!==a?a:this.M];return this.o&&this.o>=a?this.m[0]>this.ba&&(new Date).valueOf()>this.Z?this.J|this.I:this.I:this.f>=a?this.J|this.u:this.u},getProgress:function(a){a=this.T[a?a:this.M];return this.o>=a?1:this.f>a?1:this.f/a},startCollectors:function(){if(!this.D){this.a={loadTimeCollector:B(this,this.ma),mouseCollector:B(this,this.oa),keyboardCollector:B(this,this.la),accelerometerCollector:B(this,this.ea),touchCollector:B(this,this.qa)};if(window.addEventListener)window.addEventListener("load", +this.a.loadTimeCollector,!1),window.addEventListener("mousemove",this.a.mouseCollector,!1),window.addEventListener("keypress",this.a.keyboardCollector,!1),window.addEventListener("devicemotion",this.a.accelerometerCollector,!1),window.addEventListener("touchmove",this.a.touchCollector,!1);else if(document.attachEvent)document.attachEvent("onload",this.a.loadTimeCollector),document.attachEvent("onmousemove",this.a.mouseCollector),document.attachEvent("keypress",this.a.keyboardCollector);else throw new sjcl.exception.bug("can't attach event"); +this.D=!0}},stopCollectors:function(){this.D&&(window.removeEventListener?(window.removeEventListener("load",this.a.loadTimeCollector,!1),window.removeEventListener("mousemove",this.a.mouseCollector,!1),window.removeEventListener("keypress",this.a.keyboardCollector,!1),window.removeEventListener("devicemotion",this.a.accelerometerCollector,!1),window.removeEventListener("touchmove",this.a.touchCollector,!1)):document.detachEvent&&(document.detachEvent("onload",this.a.loadTimeCollector),document.detachEvent("onmousemove", +this.a.mouseCollector),document.detachEvent("keypress",this.a.keyboardCollector)),this.D=!1)},addEventListener:function(a,b){this.K[a][this.ga++]=b},removeEventListener:function(a,b){var c,d,e=this.K[a],f=[];for(d in e)e.hasOwnProperty(d)&&e[d]===b&&f.push(d);for(c=0;cb&&(a.h[b]=a.h[b]+1|0,!a.h[b]);b++);return a.L.encrypt(a.h)} +function B(a,b){return function(){b.apply(a,arguments)}}sjcl.random=new sjcl.prng(6); +a:try{var D,E,F,G;if(G="undefined"!==typeof module&&module.exports){var H;try{H=null}catch(a){H=null}G=E=H}if(G&&E.randomBytes)D=E.randomBytes(128),D=new Uint32Array((new Uint8Array(D)).buffer),sjcl.random.addEntropy(D,1024,"crypto['randomBytes']");else if("undefined"!==typeof window&&"undefined"!==typeof Uint32Array){F=new Uint32Array(32);if(window.crypto&&window.crypto.getRandomValues)window.crypto.getRandomValues(F);else if(window.msCrypto&&window.msCrypto.getRandomValues)window.msCrypto.getRandomValues(F); +else break a;sjcl.random.addEntropy(F,1024,"crypto['getRandomValues']")}}catch(a){"undefined"!==typeof window&&window.console&&(console.log("There was an error collecting entropy from the browser:"),console.log(a))} +sjcl.json={defaults:{v:1,iter:1E4,ks:128,ts:64,mode:"ccm",adata:"",cipher:"aes"},ja:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json,f=e.g({iv:sjcl.random.randomWords(4,0)},e.defaults),g;e.g(f,c);c=f.adata;"string"===typeof f.salt&&(f.salt=sjcl.codec.base64.toBits(f.salt));"string"===typeof f.iv&&(f.iv=sjcl.codec.base64.toBits(f.iv));if(!sjcl.mode[f.mode]||!sjcl.cipher[f.cipher]||"string"===typeof a&&100>=f.iter||64!==f.ts&&96!==f.ts&&128!==f.ts||128!==f.ks&&192!==f.ks&&0x100!==f.ks||2>f.iv.length|| +4=b.iter||64!==b.ts&&96!==b.ts&&128!==b.ts||128!==b.ks&&192!==b.ks&&0x100!==b.ks||!b.iv||2>b.iv.length||4 /var/www/joplin/ReactNativeClient/lib/vendor/sjcl.js +"use strict";var sjcl={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}}; +sjcl.cipher.aes=function(a){this.s[0][0][0]||this.O();var b,c,d,e,f=this.s[0][4],g=this.s[1];b=a.length;var h=1;if(4!==b&&6!==b&&8!==b)throw new sjcl.exception.invalid("invalid aes key size");this.b=[d=a.slice(0),e=[]];for(a=b;a<4*b+28;a++){c=d[a-1];if(0===a%b||8===b&&4===a%b)c=f[c>>>24]<<24^f[c>>16&255]<<16^f[c>>8&255]<<8^f[c&255],0===a%b&&(c=c<<8^c>>>24^h<<24,h=h<<1^283*(h>>7));d[a]=d[a-b]^c}for(b=0;a;b++,a--)c=d[b&3?a:a-4],e[b]=4>=a||4>b?c:g[0][f[c>>>24]]^g[1][f[c>>16&255]]^g[2][f[c>>8&255]]^g[3][f[c& +255]]}; +sjcl.cipher.aes.prototype={encrypt:function(a){return t(this,a,0)},decrypt:function(a){return t(this,a,1)},s:[[[],[],[],[],[]],[[],[],[],[],[]]],O:function(){var a=this.s[0],b=this.s[1],c=a[4],d=b[4],e,f,g,h=[],k=[],l,n,m,p;for(e=0;0x100>e;e++)k[(h[e]=e<<1^283*(e>>7))^e]=e;for(f=g=0;!c[f];f^=l||1,g=k[g]||1)for(m=g^g<<1^g<<2^g<<3^g<<4,m=m>>8^m&255^99,c[f]=m,d[m]=f,n=h[e=h[l=h[f]]],p=0x1010101*n^0x10001*e^0x101*l^0x1010100*f,n=0x101*h[m]^0x1010100*m,e=0;4>e;e++)a[e][f]=n=n<<24^n>>>8,b[e][m]=p=p<<24^p>>>8;for(e= +0;5>e;e++)a[e]=a[e].slice(0),b[e]=b[e].slice(0)}}; +function t(a,b,c){if(4!==b.length)throw new sjcl.exception.invalid("invalid aes block size");var d=a.b[c],e=b[0]^d[0],f=b[c?3:1]^d[1],g=b[2]^d[2];b=b[c?1:3]^d[3];var h,k,l,n=d.length/4-2,m,p=4,r=[0,0,0,0];h=a.s[c];a=h[0];var q=h[1],v=h[2],w=h[3],x=h[4];for(m=0;m>>24]^q[f>>16&255]^v[g>>8&255]^w[b&255]^d[p],k=a[f>>>24]^q[g>>16&255]^v[b>>8&255]^w[e&255]^d[p+1],l=a[g>>>24]^q[b>>16&255]^v[e>>8&255]^w[f&255]^d[p+2],b=a[b>>>24]^q[e>>16&255]^v[f>>8&255]^w[g&255]^d[p+3],p+=4,e=h,f=k,g=l;for(m= +0;4>m;m++)r[c?3&-m:m]=x[e>>>24]<<24^x[f>>16&255]<<16^x[g>>8&255]<<8^x[b&255]^d[p++],h=e,e=f,f=g,g=b,b=h;return r} +sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.$(a.slice(b/32),32-(b&31)).slice(1);return void 0===c?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var d=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-d^a[b/32+1|0]>>>d:a[b/32|0]>>>d)&(1<>b-1,1));return a},partial:function(a,b,c){return 32===a?b:(c?b|0:b<<32-a)+0x10000000000*a},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return!1;var c=0,d;for(d=0;d>>b),c=a[e]<<32-b;e=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(e);d.push(sjcl.bitArray.partial(b+a&31,32>>24|c>>>8&0xff00|(c&0xff00)<<8|c<<24;return a}}; +sjcl.codec.utf8String={fromBits:function(a){var b="",c=sjcl.bitArray.bitLength(a),d,e;for(d=0;d>>8>>>8>>>8),e<<=8;return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,d=0;for(c=0;c>>g)>>>e),gn){if(!b)try{return sjcl.codec.base32hex.toBits(a)}catch(p){}throw new sjcl.exception.invalid("this isn't "+m+"!");}h>e?(h-=e,f.push(l^n>>>h),l=n<>>e)>>>26),6>e?(g=a[c]<<6-e,e+=26,c++):(g<<=6,e-=6);for(;d.length&3&&!b;)d+="=";return d},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],d,e=0,f=sjcl.codec.base64.B,g=0,h;b&&(f=f.substr(0,62)+"-_");for(d=0;dh)throw new sjcl.exception.invalid("this isn't base64!");26>>e),g=h<<32-e):(e+=6,g^=h<<32-e)}e&56&&c.push(sjcl.bitArray.partial(e&56,g,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.b[0]||this.O();a?(this.F=a.F.slice(0),this.A=a.A.slice(0),this.l=a.l):this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()}; +sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.F=this.Y.slice(0);this.A=[];this.l=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.A=sjcl.bitArray.concat(this.A,a);b=this.l;a=this.l=b+sjcl.bitArray.bitLength(a);if(0x1fffffffffffffb;c++){e=!0;for(d=2;d*d<=c;d++)if(0===c%d){e= +!1;break}e&&(8>b&&(this.Y[b]=a(Math.pow(c,.5))),this.b[b]=a(Math.pow(c,1/3)),b++)}}}; +function u(a,b){var c,d,e,f=a.F,g=a.b,h=f[0],k=f[1],l=f[2],n=f[3],m=f[4],p=f[5],r=f[6],q=f[7];for(c=0;64>c;c++)16>c?d=b[c]:(d=b[c+1&15],e=b[c+14&15],d=b[c&15]=(d>>>7^d>>>18^d>>>3^d<<25^d<<14)+(e>>>17^e>>>19^e>>>10^e<<15^e<<13)+b[c&15]+b[c+9&15]|0),d=d+q+(m>>>6^m>>>11^m>>>25^m<<26^m<<21^m<<7)+(r^m&(p^r))+g[c],q=r,r=p,p=m,m=n+d|0,n=l,l=k,k=h,h=d+(k&l^n&(k^l))+(k>>>2^k>>>13^k>>>22^k<<30^k<<19^k<<10)|0;f[0]=f[0]+h|0;f[1]=f[1]+k|0;f[2]=f[2]+l|0;f[3]=f[3]+n|0;f[4]=f[4]+m|0;f[5]=f[5]+p|0;f[6]=f[6]+r|0;f[7]= +f[7]+q|0} +sjcl.mode.ccm={name:"ccm",G:[],listenProgress:function(a){sjcl.mode.ccm.G.push(a)},unListenProgress:function(a){a=sjcl.mode.ccm.G.indexOf(a);-1k)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(f=2;4>f&&l>>>8*f;f++);f<15-k&&(f=15-k);c=h.clamp(c, +8*(15-f));b=sjcl.mode.ccm.V(a,b,c,d,e,f);g=sjcl.mode.ccm.C(a,g,c,b,e,f);return h.concat(g.data,g.tag)},decrypt:function(a,b,c,d,e){e=e||64;d=d||[];var f=sjcl.bitArray,g=f.bitLength(c)/8,h=f.bitLength(b),k=f.clamp(b,h-e),l=f.bitSlice(b,h-e),h=(h-e)/8;if(7>g)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(b=2;4>b&&h>>>8*b;b++);b<15-g&&(b=15-g);c=f.clamp(c,8*(15-b));k=sjcl.mode.ccm.C(a,k,c,l,e,b);a=sjcl.mode.ccm.V(a,k.data,c,d,e,b);if(!f.equal(k.tag,a))throw new sjcl.exception.corrupt("ccm: tag doesn't match"); +return k.data},na:function(a,b,c,d,e,f){var g=[],h=sjcl.bitArray,k=h.i;d=[h.partial(8,(b.length?64:0)|d-2<<2|f-1)];d=h.concat(d,c);d[3]|=e;d=a.encrypt(d);if(b.length)for(c=h.bitLength(b)/8,65279>=c?g=[h.partial(16,c)]:0xffffffff>=c&&(g=h.concat([h.partial(16,65534)],[c])),g=h.concat(g,b),b=0;be||16n&&(sjcl.mode.ccm.fa(g/ +k),n+=m),c[3]++,e=a.encrypt(c),b[g]^=e[0],b[g+1]^=e[1],b[g+2]^=e[2],b[g+3]^=e[3];return{tag:d,data:h.clamp(b,l)}}}; +sjcl.mode.ocb2={name:"ocb2",encrypt:function(a,b,c,d,e,f){if(128!==sjcl.bitArray.bitLength(c))throw new sjcl.exception.invalid("ocb iv must be 128 bits");var g,h=sjcl.mode.ocb2.S,k=sjcl.bitArray,l=k.i,n=[0,0,0,0];c=h(a.encrypt(c));var m,p=[];d=d||[];e=e||64;for(g=0;g+4e.bitLength(c)&&(h=f(h,d(h)),c=e.concat(c,[-2147483648,0,0,0]));g=f(g,c); +return a.encrypt(f(d(f(h,d(h))),g))},S:function(a){return[a[0]<<1^a[1]>>>31,a[1]<<1^a[2]>>>31,a[2]<<1^a[3]>>>31,a[3]<<1^135*(a[0]>>>31)]}}; +sjcl.mode.gcm={name:"gcm",encrypt:function(a,b,c,d,e){var f=b.slice(0);b=sjcl.bitArray;d=d||[];a=sjcl.mode.gcm.C(!0,a,f,d,c,e||128);return b.concat(a.data,a.tag)},decrypt:function(a,b,c,d,e){var f=b.slice(0),g=sjcl.bitArray,h=g.bitLength(f);e=e||128;d=d||[];e<=h?(b=g.bitSlice(f,h-e),f=g.bitSlice(f,0,h-e)):(b=f,f=[]);a=sjcl.mode.gcm.C(!1,a,f,d,c,e);if(!g.equal(a.tag,b))throw new sjcl.exception.corrupt("gcm: tag doesn't match");return a.data},ka:function(a,b){var c,d,e,f,g,h=sjcl.bitArray.i;e=[0,0, +0,0];f=b.slice(0);for(c=0;128>c;c++){(d=0!==(a[Math.floor(c/32)]&1<<31-c%32))&&(e=h(e,f));g=0!==(f[3]&1);for(d=3;0>>1|(f[d-1]&1)<<31;f[0]>>>=1;g&&(f[0]^=-0x1f000000)}return e},j:function(a,b,c){var d,e=c.length;b=b.slice(0);for(d=0;de&&(a=b.hash(a));for(d=0;dd||0>c)throw new sjcl.exception.invalid("invalid params to pbkdf2");"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));"string"===typeof b&&(b=sjcl.codec.utf8String.toBits(b));e=e||sjcl.misc.hmac;a=new e(a);var f,g,h,k,l=[],n=sjcl.bitArray;for(k=1;32*l.length<(d||1);k++){e=f=a.encrypt(n.concat(b,[k]));for(g=1;gg;g++)e.push(0x100000000*Math.random()|0);for(g=0;g=1<this.o&&(this.o= +f);this.P++;this.b=sjcl.hash.sha256.hash(this.b.concat(e));this.L=new sjcl.cipher.aes(this.b);for(d=0;4>d&&(this.h[d]=this.h[d]+1|0,!this.h[d]);d++);}for(d=0;d>>1;this.c[g].update([d,this.N++,2,b,f,a.length].concat(a))}break;case "string":void 0===b&&(b=a.length);this.c[g].update([d,this.N++,3,b,f,a.length]);this.c[g].update(a);break;default:k=1}if(k)throw new sjcl.exception.bug("random: addEntropy only supports number, array of numbers or string");this.m[g]+=b;this.f+=b;h===this.u&&(this.isReady()!==this.u&&A("seeded",Math.max(this.o,this.f)),A("progress",this.getProgress()))}, +isReady:function(a){a=this.T[void 0!==a?a:this.M];return this.o&&this.o>=a?this.m[0]>this.ba&&(new Date).valueOf()>this.Z?this.J|this.I:this.I:this.f>=a?this.J|this.u:this.u},getProgress:function(a){a=this.T[a?a:this.M];return this.o>=a?1:this.f>a?1:this.f/a},startCollectors:function(){if(!this.D){this.a={loadTimeCollector:B(this,this.ma),mouseCollector:B(this,this.oa),keyboardCollector:B(this,this.la),accelerometerCollector:B(this,this.ea),touchCollector:B(this,this.qa)};if(window.addEventListener)window.addEventListener("load", +this.a.loadTimeCollector,!1),window.addEventListener("mousemove",this.a.mouseCollector,!1),window.addEventListener("keypress",this.a.keyboardCollector,!1),window.addEventListener("devicemotion",this.a.accelerometerCollector,!1),window.addEventListener("touchmove",this.a.touchCollector,!1);else if(document.attachEvent)document.attachEvent("onload",this.a.loadTimeCollector),document.attachEvent("onmousemove",this.a.mouseCollector),document.attachEvent("keypress",this.a.keyboardCollector);else throw new sjcl.exception.bug("can't attach event"); +this.D=!0}},stopCollectors:function(){this.D&&(window.removeEventListener?(window.removeEventListener("load",this.a.loadTimeCollector,!1),window.removeEventListener("mousemove",this.a.mouseCollector,!1),window.removeEventListener("keypress",this.a.keyboardCollector,!1),window.removeEventListener("devicemotion",this.a.accelerometerCollector,!1),window.removeEventListener("touchmove",this.a.touchCollector,!1)):document.detachEvent&&(document.detachEvent("onload",this.a.loadTimeCollector),document.detachEvent("onmousemove", +this.a.mouseCollector),document.detachEvent("keypress",this.a.keyboardCollector)),this.D=!1)},addEventListener:function(a,b){this.K[a][this.ga++]=b},removeEventListener:function(a,b){var c,d,e=this.K[a],f=[];for(d in e)e.hasOwnProperty(d)&&e[d]===b&&f.push(d);for(c=0;cb&&(a.h[b]=a.h[b]+1|0,!a.h[b]);b++);return a.L.encrypt(a.h)} +function B(a,b){return function(){b.apply(a,arguments)}}sjcl.random=new sjcl.prng(6); +a:try{var D,E,F,G;if(G="undefined"!==typeof module&&module.exports){var H;try{H=require("crypto")}catch(a){H=null}G=E=H}if(G&&E.randomBytes)D=E.randomBytes(128),D=new Uint32Array((new Uint8Array(D)).buffer),sjcl.random.addEntropy(D,1024,"crypto['randomBytes']");else if("undefined"!==typeof window&&"undefined"!==typeof Uint32Array){F=new Uint32Array(32);if(window.crypto&&window.crypto.getRandomValues)window.crypto.getRandomValues(F);else if(window.msCrypto&&window.msCrypto.getRandomValues)window.msCrypto.getRandomValues(F); +else break a;sjcl.random.addEntropy(F,1024,"crypto['getRandomValues']")}}catch(a){"undefined"!==typeof window&&window.console&&(console.log("There was an error collecting entropy from the browser:"),console.log(a))} +sjcl.json={defaults:{v:1,iter:1E4,ks:128,ts:64,mode:"ccm",adata:"",cipher:"aes"},ja:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json,f=e.g({iv:sjcl.random.randomWords(4,0)},e.defaults),g;e.g(f,c);c=f.adata;"string"===typeof f.salt&&(f.salt=sjcl.codec.base64.toBits(f.salt));"string"===typeof f.iv&&(f.iv=sjcl.codec.base64.toBits(f.iv));if(!sjcl.mode[f.mode]||!sjcl.cipher[f.cipher]||"string"===typeof a&&100>=f.iter||64!==f.ts&&96!==f.ts&&128!==f.ts||128!==f.ks&&192!==f.ks&&0x100!==f.ks||2>f.iv.length|| +4=b.iter||64!==b.ts&&96!==b.ts&&128!==b.ts||128!==b.ks&&192!==b.ks&&0x100!==b.ks||!b.iv||2>b.iv.length||4
+ +
+ +

oplin

+

An open source note taking and to-do application with synchronisation capabilities.

+
+ + + +
+

About End-To-End Encryption (E2EE)

+
    +
  1. Now you need to synchronise all your notes so that thEnd-to-end encryption (E2EE) is a system where only the owner of the notes, notebooks, tags or resources can read them. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developer of Joplin from being able to access the data.
  2. +
+

The systems is designed to defeat any attempts at surveillance or tampering because no third parties can decipher the data being communicated or stored.

+

There is a small overhead to using E2EE since data constantly have to be encrypted and decrypted so consider whether you really need the feature.

+

Enabling E2EE

+

Due to the decentralised nature of Joplin, E2EE needs to be manually enabled on all the applications that you synchronise with. It is recommended to first enable it on the desktop or terminal application since they generally run on more powerful devices (unlike the mobile application), and so they can encrypt the initial data faster.

+

To enable it, please follow these steps:

+
    +
  1. On your first device (eg. on the desktop application), go to the Encryption Config screen and click "Enable encryption"
  2. +
  3. Input your password. This is the Master Key password which will be used to encrypt all your notes. Make sure you do not forget it since, for security reason, it cannot be recovered. +ey are sent encrypted to the sync target (eg. to OneDrive, Nextcloud, etc.). Wait for any synchronisation that might be in progress and click on "Synchronise".
  4. +
  5. Wait for this synchronisation operation to complete. Since all the data needs to be re-sent (encrypted) to the sync target, it may take a long time, especially if you have many notes and resources. Note that even if synchronisation seems stuck, most likely it is still running - do not cancel it and simply let it run over night if needed.
  6. +
  7. Once this first synchronisation operation is done, open the next device you are synchronising with. Click "Synchronise" and wait for the sync operation to complete. The device will receive the master key, and you will need to provide the password for it. At this point E2EE will be automatically enabled on this device. Once done, click Synchronise again and wait for it to complete.
  8. +
  9. Repeat step 5 for each device.
  10. +
+

Once all the devices are in sync with E2EE enabled, the encryption/decryption should be mostly transparent. Occasionally you may see encrypted items but they will get decrypted in the background eventually.

+

Disabling E2EE

+

Follow the same procedure as above but instead disable E2EE on each device one by one. Again it might be simpler to do it one device at a time and to wait every time for the synchronisation to complete.

+ + + + + diff --git a/docs/help/spec.html b/docs/help/spec.html new file mode 100644 index 000000000..ee8fc71df --- /dev/null +++ b/docs/help/spec.html @@ -0,0 +1,314 @@ + + + + Joplin - an open source note taking and to-do application with synchronisation capabilities + + + + + + + + + + + + +
+ +
+ +

oplin

+

An open source note taking and to-do application with synchronisation capabilities.

+
+ + + +
+

Encryption

+

Encrypted data is encoded to ASCII because encryption/decryption functions in React Native can only deal with strings. So for compatibility with all the apps we need to use the lowest common denominator.

+

Encrypted data format

+ + + + + + + + + + + + + + + + + + +
NameSize
Identifier3 chars ("JED")
Version number2 chars (Hexa string)
+

This is followed by the encryption metadata:

+ + + + + + + + + + + + + + + + + + + + + +
NameSize
Length6 chars (Hexa string)
Encryption method2 chars (Hexa string)
Master key ID32 chars (Hexa string)
+

See lib/services/EncryptionService.js for the list of available encryption methods.

+

Data chunk

+

The data is encoded in one or more chuncks for performance reasons. That way it is possible to take a block of data from one file and encrypt it to another block in another file. Encrypting/decrypting the whole file in one go would not work (on mobile especially).

+ + + + + + + + + + + + + + + + + +
NameSize
Length6 chars (Hexa string)
Data("Length" bytes) (ASCII)
+

Master Keys

+

The master keys are used to encrypt and decrypt data. They can be generated from the Encryption Service, and are saved to the database. They are themselves encrypted via a user password.

+

These encrypted master keys are transmitted with the sync data so that they can be available to each client. Each client will need to supply the user password to decrypt each key.

+

The application supports multiple master keys in order to handle cases where one offline client starts encrypting notes, then another offline client starts encrypting notes too, and later both sync. Both master keys will have to be decrypted separately with the user password.

+

Only one master key can be active for encryption purposes. For decryption, the algorithm will check the Master Key ID in the header, then check if it's available to the current app and, if so, use this for decryption.

+

Encryption Service

+

The applications make use of the EncryptionService class to handle encryption and decryption. Before it can be used, a least one master key must be loaded into it and marked as "active".

+

Encryption workflow

+

Items are encrypted only during synchronisation, when they are serialised (via BaseItem.serializeForSync), so before being sent to the sync target.

+

They are decrypted by DecryptionWorker in the background.

+

The apps handle displaying both decrypted and encrypted items, so that user is aware that these items are there even if not yet decrypted. Encrypted items are mostly read-only to the user, except that they can be deleted.

+

Enabling and disabling encryption

+

Enabling/disabling E2EE while two clients are in sync might have an unintuitive behaviour (although that behaviour might be correct), so below some scenarios are explained:

+
    +
  • If client 1 enables E2EE, all items will be synced to target and will appear encrypted on target. Although all items have been re-uploaded to the target, their timestamps did not change (because the item data itself has not changed, only its representation). Because of this, client 2 will not re-download the items - it does not need to do so anyway since it has already the item data.

    +
  • +
  • When a client sync and download a master key for the first time, encryption will be automatically enabled (user will need to supply the master key password). In that case, all items that are not encrypted will be re-synced. Uploading only non-encrypted items is an optimisation since if an item is already encrypted locally it means it's encrypted on target too.

    +
  • +
  • If both clients are in sync with E2EE enabled: if client 1 disable E2EE, it's going to re-upload all the items unencrypted. Client 2 again will not re-download the items for the same reason as above (data did not change, only representation). Note that user must manually disable E2EE on all clients otherwise some will continue to upload encrypted items. Since synchronisation is stateless, clients do not know whether other clients use E2EE or not so this step has to be manual.

    +
  • +
  • Although messy, Joplin supports having some clients send encrypted items and others unencrypted ones. The situation gets resolved once all the clients have the same E2EE settings.

    +
  • +
+ + + + + diff --git a/docs/index.html b/docs/index.html index 564e806aa..a753c6bc5 100644 --- a/docs/index.html +++ b/docs/index.html @@ -218,15 +218,15 @@ Windows -Get it on Windows +Get it on Windows macOS -Get it on macOS +Get it on macOS Linux -Get it on macOS +Get it on macOS @@ -251,7 +251,7 @@

Terminal application

On macOS:

-
brew install node joplin
+
brew install joplin
 

On Linux or Windows (via WSL):

Important: First, install Node 8+. Node 8 is LTS but not yet available everywhere so you might need to manually install it.

NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin
@@ -284,6 +284,12 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
 

To import Evernote data, first export your Evernote notebooks to ENEX files as described here. Then follow these steps:

On the desktop application, open the "File" menu, click "Import Evernote notes" and select your ENEX file. This will open a new screen which will display the import progress. The notes will be imported into a new separate notebook (so that, in case of a mistake, the notes are not mixed up with any existing notes). If needed then can then be moved to a different notebook, or the notebook can be renamed, etc.

On the terminal application, in command-line mode, type import-enex /path/to/file.enex. This will import the notes into a new notebook named after the filename.

+

Importing notes from other applications

+

In general the way to import notes from any application into Joplin is to convert the notes to ENEX files (Evernote format) and to import these ENEX files into Joplin using the method above. Most note-taking applications support ENEX files so it should be relatively straightforward. For help about specific applications, see below:

+
    +
  • Standard Notes: Please see this tutorial
  • +
  • Tomboy Notes: Export the notes to ENEX files as described here for example, and import these ENEX files into Joplin.
  • +

Synchronisation

One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as OneDrive or Dropbox, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.

Currently, synchronisation is possible with OneDrive (by default) or the local filesystem. A NextCloud driver, and a Dropbox one will also be available once this React Native bug is fixed. When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.

@@ -303,7 +309,7 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin

On mobile, the alarms will be displayed using the built-in notification system.

If for any reason the notifications do not work, please open an issue.

Localisation

-

Joplin is currently available in English, French and Spanish. If you would like to contribute a translation, it is quite straightforward, please follow these steps:

+

Joplin is currently available in English, French, Spanish, German, Portuguese, Chinese, Japanese, Russian, Croatian and Italian. If you would like to contribute a translation, it is quite straightforward, please follow these steps:

  • Download Poedit, the translation editor, and install it.
  • Download the file to be translated.
  • @@ -327,7 +333,7 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
  • While the mobile can sync and load tags, it is not currently possible to create new ones. The desktop and terminal apps can create, delete and edit tags.

License

-

Copyright (c) 2016-2017 Laurent Cozic

+

Copyright (c) 2016-2018 Laurent Cozic

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

diff --git a/docs/terminal/index.html b/docs/terminal/index.html index 6ed642d5d..b6604b4dc 100644 --- a/docs/terminal/index.html +++ b/docs/terminal/index.html @@ -205,7 +205,7 @@

Installation

On macOS:

-
brew install node joplin
+
brew install joplin
 

On Linux or Windows (via WSL):

Important: First, install Node 8+. Node 8 is LTS but not yet available everywhere so you might need to manually install it.

NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin
@@ -516,7 +516,7 @@ version
 
     Displays version information
 

License

-

Copyright (c) 2016-2017 Laurent Cozic

+

Copyright (c) 2016-2018 Laurent Cozic

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.