Release / Build and publish image (push) Failing after 9m59s
Panelet under Integrationer: tilføj, hent nu, kobl fra. Sit eget frem for en tilstand mere inde i Gmail-panelet, for de to er ikke det samme i forskellige klæder — det ene er en app, man logger ind igennem, det andet et kodeord, man selv holder. Feltet hedder IMAP-server og ikke Server: der stod i forvejen Server på CalDAV længere nede, og to felter med samme navn på én side er tvetydige også for en skærmlæser. Testen fandt det, men det var ikke kun testens problem. To vagter fangede mangler undervejs, og begge havde ret: mailbox_failed skulle have en tekst i fladen, ellers ville nogen få serverens engelske logsprog at se, og de tre ruter skulle i openapi.yaml. Der er nu et Mailbox-skema samme sted, som siger hvad der ikke kommer tilbage — kodeordet og eventuelle tokens. E2E-prøven forbinder til noget, der ikke lytter, og ser efter værtens egen afvisning på skærmen. Den er samtidig prøven på at 422 var det rigtige valg: var det stadig 502, ville et mellemled kunne kaste beskeden væk, og der ville stå "noget gik galt". Co-Authored-By: Claude Opus 5 <[email protected]>
696 lines
32 KiB
JavaScript
696 lines
32 KiB
JavaScript
/**
|
||
* English, translated from `da.js`.
|
||
*
|
||
* Danish is the source: it is the language the app was written in, and the one
|
||
* whose phrasing was argued over. A key missing here falls through to Danish
|
||
* rather than to the key itself — a Danish sentence in an English screen can be
|
||
* read, and `settings.notifications.title` cannot.
|
||
*
|
||
* Dates are not here. They go through `tag()`, which returns `en-GB` for this
|
||
* locale: the app is day-month everywhere, weeks start on Monday, and an American
|
||
* date format inside a European layout reads as a bug.
|
||
*/
|
||
export const en = {
|
||
// --- the shell ---------------------------------------------------------------
|
||
'nav.today': 'Today',
|
||
'nav.upcoming': 'Upcoming',
|
||
'nav.delegated': 'Waiting on others',
|
||
'nav.inbox': 'Inbox',
|
||
'nav.settings': 'Settings',
|
||
'nav.signOut': 'Sign out',
|
||
'nav.main': 'Main menu',
|
||
'nav.search': 'Search',
|
||
'nav.searchLong': 'Search tasks and projects',
|
||
'nav.noResults': 'Nothing found.',
|
||
'nav.showSidebar': 'Show the sidebar',
|
||
'nav.hideSidebar': 'Hide the sidebar',
|
||
'nav.showMenu': 'Show menu',
|
||
'nav.sidebarWidth': 'Sidebar width',
|
||
'nav.toggleTheme': 'Switch between light and dark',
|
||
'nav.offline': 'Offline',
|
||
'nav.offlineHint': 'Changes from other people are not showing right now',
|
||
'nav.doItToday': 'Do it today',
|
||
|
||
// --- the sidebar's own sections ------------------------------------------------
|
||
'sidebar.projects': 'Projects',
|
||
'sidebar.shared': 'Shared with me',
|
||
'sidebar.filters': 'Filters',
|
||
'sidebar.labels': 'Labels',
|
||
'sidebar.newProject': 'New project',
|
||
'sidebar.newGroup': 'New group',
|
||
'sidebar.projectName': 'Project name',
|
||
'sidebar.groupName': "The group's name",
|
||
'sidebar.noProjects': 'No projects yet.',
|
||
'sidebar.emptyGroup': 'Empty — drag a project up here.',
|
||
'sidebar.rename': 'Rename',
|
||
'sidebar.delete': 'Delete',
|
||
'sidebar.fold': 'Collapse {name}',
|
||
'sidebar.unfold': 'Expand {name}',
|
||
'sidebar.deleteGroup': 'Delete the group "{name}"?',
|
||
'sidebar.deleteGroupOne': 'The project in it stays, and ends up without a group.',
|
||
'sidebar.deleteGroupMany': 'The {n} projects stay, and end up without a group.',
|
||
|
||
// --- a task in a list ----------------------------------------------------------
|
||
'task.add': 'Add',
|
||
'task.new': 'New task',
|
||
'task.syntaxProject': 'project',
|
||
'task.syntaxSection': 'section',
|
||
'task.syntaxLabel': 'label',
|
||
'task.syntaxPriority': 'priority',
|
||
'task.syntaxDateMark': 'tomorrow at 10',
|
||
'task.syntaxDate': 'due',
|
||
'task.placeholder': 'Add a task — try “pay VAT tomorrow at 10 p1 #Company”',
|
||
'task.complete': 'Mark as done',
|
||
'task.reopen': 'Reopen task',
|
||
'task.today': 'Today',
|
||
'task.tomorrow': 'Tomorrow',
|
||
'task.yesterday': 'Yesterday',
|
||
'task.overdue': '{n} days overdue',
|
||
'task.repeats': 'Repeats {rule}',
|
||
'task.assignee': 'Assigned to {name}',
|
||
'task.subtasks': '{done} of {total} sub-tasks are done',
|
||
'task.attachments': '{n} attached',
|
||
'task.priority': 'Priority {n}',
|
||
'task.noPriority': 'No priority',
|
||
|
||
// --- a project group -----------------------------------------------------------
|
||
'group.colorOf': 'Colour of {name}',
|
||
|
||
|
||
// --- settings: the frame -------------------------------------------------------
|
||
'settings.title': 'Settings',
|
||
'settings.tab.account': 'Account',
|
||
'settings.tab.notifications': 'Notifications',
|
||
'settings.tab.integrations': 'Integrations',
|
||
'settings.tab.ai': 'AI',
|
||
'settings.tab.tokens': 'API tokens',
|
||
'settings.tab.users': 'People',
|
||
'settings.tab.history': 'History',
|
||
'settings.tab.errors': 'Errors',
|
||
'settings.tab.data': 'Data and templates',
|
||
|
||
// --- settings: the account -----------------------------------------------------
|
||
'account.profile': 'Profile',
|
||
'account.profileHint':
|
||
'The timezone is not decoration: every date in the app is resolved in it, so “tomorrow at 9” means something else once you change it.',
|
||
'account.name': 'Name',
|
||
'account.email': 'Email',
|
||
'account.emailFixed': 'Cannot be changed here.',
|
||
'account.timezone': 'Timezone',
|
||
'account.parseLanguage': 'Language',
|
||
'account.danish': 'Dansk',
|
||
'account.english': 'English',
|
||
'account.parseHint':
|
||
'Both the interface’s language and the grammar quick add reads a line with — “hver mandag” or “every monday”.',
|
||
'account.save': 'Save',
|
||
'account.saved': 'Saved.',
|
||
'account.appearance': 'Appearance',
|
||
'account.appearanceHint':
|
||
'Kept in this browser. It is a property of the screen you are sitting at — a laptop in the sun and a monitor in a dark room want different answers.',
|
||
'account.password': 'Password',
|
||
'account.passwordHint':
|
||
'Changing it signs every other session out — which is usually the reason to do it.',
|
||
'account.currentPassword': 'Current password',
|
||
'account.newPassword': 'New password',
|
||
'account.changePassword': 'Change password',
|
||
'account.passwordChanged': 'Changed.',
|
||
'account.totp': 'Two-factor',
|
||
'account.totpHint': 'A one-time code from an authenticator app, on top of the password.',
|
||
'account.writeThemDown': 'Write them down now.',
|
||
'account.recoveryHint': 'They are shown this once — the server only has their hash.',
|
||
'account.gotThem': 'I have them',
|
||
'account.passwordRequired':
|
||
'Required for both actions below. Turning two-factor off is exactly what a stranger holding your session would do first.',
|
||
'account.newRecoveryCodes': 'New recovery codes',
|
||
'account.totpOff': 'Turn two-factor off',
|
||
'account.scanHint': 'Scan it in your authenticator app, or type the key in by hand.',
|
||
'account.codeFromApp': 'The code from the app',
|
||
'account.totpProofHint':
|
||
'Two-factor is only on once a code has proved the app really has the key.',
|
||
'account.confirm': 'Confirm',
|
||
'account.cancel': 'Cancel',
|
||
'account.totpOn': 'Turn two-factor on',
|
||
'account.devices': 'Devices',
|
||
'account.devicesHint':
|
||
'Where you are signed in. If you do not recognise one, sign it out and change your password.',
|
||
'account.thisDevice': 'this device',
|
||
'account.signOutDevice': 'Sign out',
|
||
|
||
'account.totpEnabled': 'On.',
|
||
'account.recoveryLeftOne': '{n} recovery code left.',
|
||
'account.recoveryLeftMany': '{n} recovery codes left.',
|
||
|
||
// --- settings: notifications ---------------------------------------------------
|
||
'push.title': 'Notifications on this device',
|
||
'push.hint':
|
||
'Reminders and messages from other people, even with the tab closed. The message is encrypted to your browser on the way — the push service cannot read it.',
|
||
'push.unsupported':
|
||
'This browser does not support it. Safari needs the app added to the home screen; over plain HTTP it only works on localhost.',
|
||
'push.blocked':
|
||
'The browser has blocked notifications for this site. That can only be changed in the browser’s own settings — a page cannot ask again once the answer has been no.',
|
||
'push.on': 'On for this device.',
|
||
'push.turnOff': 'Turn off',
|
||
'push.turnOn': 'Turn on for this device',
|
||
'push.perBrowser': 'Applies only to the browser you are in right now.',
|
||
'push.recent': 'Recent',
|
||
'push.none': 'Nothing yet.',
|
||
'push.open': 'Open',
|
||
'push.markAllRead': 'Mark all as read',
|
||
'push.version': 'Version',
|
||
'push.running': 'Running',
|
||
'push.seeRelease': 'See the release',
|
||
|
||
// --- settings: API tokens ------------------------------------------------------
|
||
'tokens.title': 'API tokens',
|
||
'tokens.hint':
|
||
'What a script, a CalDAV client or an MCP connector signs in with. A token carries exactly your permissions — it can do nothing you cannot.',
|
||
'tokens.copyNow': 'Copy it now.',
|
||
'tokens.onceOnly': 'It is shown this once — the server only has its hash.',
|
||
'tokens.copy': 'Copy',
|
||
'tokens.copied': 'Copied.',
|
||
'tokens.done': 'Done',
|
||
'tokens.mcpBefore': 'Using it as an',
|
||
'tokens.mcpLink': 'MCP connector',
|
||
'tokens.mcpAfter': 'in Claude? Then this is the address the dialog wants:',
|
||
'tokens.copyAddress': 'Copy the address',
|
||
'tokens.name': 'Name',
|
||
'tokens.namePlaceholder': 'e.g. my laptop, or the backup script',
|
||
'tokens.expires': 'Expires',
|
||
'tokens.never': 'Never',
|
||
'tokens.in30': 'In 30 days',
|
||
'tokens.in90': 'In 90 days',
|
||
'tokens.inYear': 'In a year',
|
||
'tokens.expiryHint':
|
||
'A calendar client that has been subscribed for years needs “never”. A script you run once does not.',
|
||
'tokens.create': 'Create a token',
|
||
'tokens.none': 'No tokens yet.',
|
||
'tokens.revoke': 'Revoke',
|
||
|
||
// --- settings: AI --------------------------------------------------------------
|
||
'ai.title': 'AI',
|
||
'ai.hint':
|
||
'Used in two places: splitting a task into sub-tasks, and writing a short overview of what is outstanding. The key stays on your server and is only sent to the provider you pick here.',
|
||
'ai.provider': 'Provider',
|
||
'ai.off': 'Off',
|
||
'ai.model': 'Model',
|
||
'ai.baseURL': 'Address',
|
||
'ai.apiKey': 'API key',
|
||
'ai.save': 'Save',
|
||
'ai.saved': 'Saved.',
|
||
'ai.weekly': 'Weekly overview',
|
||
'ai.weeklyHint': 'A short note on what looks most important right now.',
|
||
|
||
'push.unreadOne': '{n} unread.',
|
||
'push.unreadMany': '{n} unread.',
|
||
|
||
'ai.keyStored': 'A key is stored — leave this blank',
|
||
'ai.keyBlankHint':
|
||
'The field is blank on purpose. The key is never sent back to the browser — a settings page that refills a password field is one that will one day leak a key into a screenshot.',
|
||
'ai.keyNewHint': 'Stored on the server and never shown again.',
|
||
'tokens.lastUsed': 'last used {when}',
|
||
'tokens.neverUsed': 'never used',
|
||
'tokens.expired': 'expired {when}',
|
||
'tokens.expiresOn': 'expires {when}',
|
||
'tokens.revokeNamed': 'Revoke {name}',
|
||
|
||
// --- settings: people ----------------------------------------------------------
|
||
'users.invite': 'Invite somebody',
|
||
'users.inviteHint':
|
||
'There is no open registration. You send a link and the person picks their own name and password — so there is never a password two people know.',
|
||
'users.emailAddress': 'Email address',
|
||
'users.emailPlaceholder': '[email protected]',
|
||
'users.mustBeEmail': 'Must be an email address.',
|
||
'users.sendInvite': 'Send invitation',
|
||
'users.inviteMade': 'The invitation is ready. Send the link to them:',
|
||
'users.pending': 'Awaiting a reply',
|
||
'users.pendingHint':
|
||
'Invitations sent and not used yet. Only the link’s hash is stored, so it cannot be shown again — if it went to the wrong person, revoke it.',
|
||
'users.revoke': 'Revoke',
|
||
'users.accounts': 'Accounts',
|
||
'users.accountsHint': 'Everybody on this instance.',
|
||
'users.admin': 'administrator',
|
||
'users.you': 'you',
|
||
'users.makeAdmin': 'Make administrator',
|
||
'users.removeAdmin': 'Remove administrator',
|
||
'users.delete': 'Delete',
|
||
'users.deleteQuestion': 'Delete {name} <{email}> for good?',
|
||
'users.deleteTakes': 'That deletes {projects} and the {tasks} in them.',
|
||
'users.deleteLeaves': '{tasks} in other people’s projects stay, without an author.',
|
||
'users.deleteFinal': 'It cannot be undone. There is no trash for accounts.',
|
||
'users.revokeQuestion': 'Revoke the invitation to {email}? The link stops working.',
|
||
'users.projectOne': 'project',
|
||
'users.projectMany': 'projects',
|
||
'users.taskOne': 'task',
|
||
'users.taskMany': 'tasks',
|
||
'users.never': 'never seen',
|
||
|
||
// --- settings: history and errors -----------------------------------------------
|
||
'history.title': 'History',
|
||
'history.hint':
|
||
'Everything that has been done on this server, across every project — including the ones you are not in. A project’s own history panel shows the same rows one project at a time; this is the question those cannot answer. Rows stay when an account is deleted: what was done stops having a name, not having happened.',
|
||
'history.person': 'Person',
|
||
'history.all': 'All',
|
||
'history.project': 'Project',
|
||
'history.event': 'Event',
|
||
'history.clear': 'Clear',
|
||
'history.more': 'Load more',
|
||
'history.loading': 'Loading…',
|
||
'history.noMatch': 'Nothing matches the filters.',
|
||
'history.nothing': 'Nothing has been recorded yet.',
|
||
'history.deletedAccount': 'A deleted account',
|
||
'errors.title': 'Server errors',
|
||
'errors.hint':
|
||
'Every 500 the API has answered, with what the handler was attempting and the error it hit. Written to the database rather than only to the log — a container’s log starts over on every restart, so the explanation is usually gone by the time anybody looks. Kept for 30 days.',
|
||
'errors.none': 'No errors recorded. That is the right state.',
|
||
|
||
// --- the views -------------------------------------------------------------------
|
||
'view.delegated': 'Waiting on others',
|
||
'view.delegatedEmpty': 'You are not waiting on anybody.',
|
||
'view.labelEmpty': 'No open tasks with this label.',
|
||
'view.filterEmpty': 'Nothing matches right now.',
|
||
'view.filterFailed': 'The filter could not be run.',
|
||
'view.upcoming': 'Upcoming',
|
||
'view.mode': 'View',
|
||
'view.list': 'List',
|
||
'view.week': 'Week',
|
||
'view.month': 'Month',
|
||
'view.board': 'Board',
|
||
'view.calendar': 'Calendar',
|
||
'view.emptySection': 'Empty',
|
||
'view.addSection': '+ Add a section',
|
||
'view.sectionName': 'The section’s name',
|
||
'view.newSection': 'New section',
|
||
'view.prevMonth': 'Previous month',
|
||
'view.nextMonth': 'Next month',
|
||
'view.prevWeek': 'Previous week',
|
||
'view.nextWeek': 'Next week',
|
||
'view.weekNumber': 'week {n}',
|
||
'view.more': '+{n} more',
|
||
|
||
// --- how long ago ----------------------------------------------------------------
|
||
'when.justNow': 'just now',
|
||
'when.minutes': '{n} min. ago',
|
||
'when.hours': '{n} hours ago',
|
||
'when.days': '{n} days ago',
|
||
'when.never': 'never',
|
||
'when.neverSignedIn': 'never signed in',
|
||
|
||
// --- settings: data ---------------------------------------------------------------
|
||
'data.import': 'Import from Todoist',
|
||
'data.csvFile': 'CSV file',
|
||
'data.importing': 'Importing …',
|
||
'data.openProject': 'Open the project',
|
||
'data.trash': 'Trash',
|
||
'data.trashEmpty': 'The trash is empty.',
|
||
'data.restore': 'Restore',
|
||
'data.backups': 'Backups',
|
||
'data.backupNow': 'Take one now',
|
||
'data.backingUp': 'Copying…',
|
||
'data.backupMade': 'Backup written — {size}',
|
||
'data.backupFailed': 'failed',
|
||
'data.download': 'Download',
|
||
'data.noBackups': 'No backup has been taken yet.',
|
||
'data.swept': 'swept away',
|
||
'data.empty': 'empty',
|
||
'data.export': 'Export',
|
||
'data.exportAccount': 'The whole account as JSON',
|
||
'data.exportOne': 'One project',
|
||
'data.pickProject': 'Pick a project',
|
||
'data.csv': 'CSV',
|
||
'data.ics': 'Calendar file',
|
||
'data.templates': 'Templates',
|
||
'data.saveAsTemplate': 'Save a project as a template',
|
||
'data.templateName': 'Name for the template',
|
||
'data.saveTemplate': 'Save as a template',
|
||
'data.noTemplates': 'No templates yet.',
|
||
'data.use': 'Use',
|
||
'data.delete': 'Delete',
|
||
'data.newProjectName': 'Name for the new project',
|
||
'data.startDate': 'Start date',
|
||
'data.startDateHint': 'Day zero for the template’s dates. Blank means today.',
|
||
'data.createProject': 'Create the project',
|
||
|
||
// --- a group's own page ------------------------------------------------------------
|
||
'group.notFound': 'That group does not exist, or it is not yours.',
|
||
'group.failed': 'The group could not be loaded.',
|
||
'group.retry': 'Try again',
|
||
'group.about': 'About this group',
|
||
'group.aboutPlaceholder': 'What kind of work is this?',
|
||
'group.writeAbout': 'Write what kind of work this is.',
|
||
'group.saving': 'Saving…',
|
||
'group.projects': 'Projects',
|
||
'group.projectOne': '1 project',
|
||
'group.projectMany': '{n} projects',
|
||
'group.noProjects':
|
||
'No projects in the group yet. Drag one up here in the sidebar, or pick the group when you create one.',
|
||
'group.documents': 'Documents',
|
||
'group.documentsHint':
|
||
'Files that belong to the whole group rather than to one task in it — the contract, the drawing, the agreement everything else rests on.',
|
||
'group.remove': 'Remove',
|
||
'group.removeNamed': 'Remove {name}',
|
||
'group.removeQuestion': 'Remove "{name}"?',
|
||
'group.addFile': '+ Upload a file',
|
||
'group.uploading': 'Uploading…',
|
||
|
||
// --- keyboard shortcuts --------------------------------------------------------------
|
||
'keys.title': 'Shortcuts',
|
||
|
||
'data.importHint':
|
||
'Pick the CSV export of one project. Priorities are converted, not copied: Todoist writes 4 for what its interface calls P1.',
|
||
'data.trashHint':
|
||
'Deleted projects, with their tasks. They come back exactly as they were — except for tasks you had deleted individually first; there was a reason for those.',
|
||
'data.backupsHint':
|
||
'The database is copied every night and the most recent fourteen are kept. Counted, not dated: a container that has been off for a month must not come back and delete all of them for being too old.',
|
||
'data.backupsSecrets':
|
||
'Tokens and mailbox passwords are encrypted inside the copy, and the key is not: it sits in a file beside the database. That is the point — a copy that goes astray does not open your mail. But restore a copy somewhere without that file and the mailboxes must be connected again.',
|
||
'data.backupsAttachments':
|
||
'Attachments are not included. They are content-addressed and only ever added to, so fourteen identical copies would fill the volume they were meant to protect — take the whole of /data if you want everything.',
|
||
'data.restoreHint':
|
||
'Putting a copy back is deliberately not a button here: a database cannot be swapped under a server that is writing to it, and a half-finished swap destroys the very thing it was meant to save. Stop the container, put the file in place of verdande.db in the data directory, delete the -wal and -shm beside it, and start it again.',
|
||
'data.exportHint':
|
||
'Everything, in its raw form. That is the guarantee that you can leave — an export that has been tidied up is an export that has lost something.',
|
||
'data.templatesHint':
|
||
'A project saved as a shape you can use again. The dates in a template are relative, so they are worked out from the start date you pick.',
|
||
|
||
'keys.anyLetter': 'any letter',
|
||
'keys.typeToCapture': 'starts a task, with the letter already in the field',
|
||
'keys.navigation': 'Navigation',
|
||
'keys.jumpToQuickAdd': 'Jump to the “add a task” field',
|
||
'keys.thisPage': 'This page',
|
||
'keys.inQuickAdd': 'In quick add',
|
||
'keys.intoProject': 'Put the task in a project',
|
||
'keys.setLabel': 'Set a label',
|
||
'keys.priority': 'Priority',
|
||
'keys.date': 'A date — Danish and English',
|
||
'keys.time': 'A time',
|
||
'keys.clear': 'Clear the field',
|
||
|
||
// --- signing in ------------------------------------------------------------------
|
||
'auth.firstAccount': 'Create the first account. It becomes the administrator.',
|
||
'auth.name': 'Name',
|
||
'auth.nameRequired': 'Required',
|
||
'auth.email': 'Email',
|
||
'auth.emailInvalid': 'Must be an email address',
|
||
'auth.password': 'Password',
|
||
'auth.passwordHint': 'At least 10 characters. A sentence is both easier and stronger.',
|
||
'auth.createAccount': 'Create account',
|
||
'auth.totpPrompt': 'Enter the code from your authenticator.',
|
||
'auth.code': 'Code',
|
||
'auth.totpRecovery': 'If you have lost your phone, one of your recovery codes works here.',
|
||
'auth.continue': 'Continue',
|
||
'auth.resetPrompt': 'We will send a link to choose a new password.',
|
||
'auth.sendLink': 'Send the link',
|
||
'auth.back': 'Back',
|
||
'auth.linkBroken': 'That link is incomplete. Open it from the email again, or ask for a new one.',
|
||
'auth.toSignIn': 'To sign in',
|
||
'auth.signOut': 'Sign out',
|
||
'auth.invited': 'You have been invited. Choose a name and a password.',
|
||
'auth.pickNew': 'Choose a new password.',
|
||
'auth.newPassword': 'New password',
|
||
'auth.newPasswordHint': 'At least 10 characters. Every other device is signed out.',
|
||
'auth.savePassword': 'Save the password',
|
||
'auth.passwordChanged': 'The password has been changed. Sign in with the new one.',
|
||
'auth.resetSent':
|
||
'If that address has an account, a link is on its way. Check the spam filter too.',
|
||
'auth.signInPrompt': 'Sign in to continue.',
|
||
'auth.signIn': 'Sign in',
|
||
'auth.forgot': 'Forgotten your password?',
|
||
|
||
// --- a task, opened ---------------------------------------------------------------
|
||
'detail.task': 'Task',
|
||
'detail.close': 'Close',
|
||
'detail.text': 'The task’s text',
|
||
'detail.description': 'Description',
|
||
'detail.descriptionPlaceholder': 'Details, links, whatever has to be remembered',
|
||
'detail.priority': 'Priority',
|
||
'detail.p1': 'P1 — urgent',
|
||
'detail.none': 'None',
|
||
'detail.due': 'Due',
|
||
'detail.labels': 'Labels',
|
||
'detail.labelsPlaceholder': 'separated by commas',
|
||
'detail.project': 'Project',
|
||
'detail.repeats': 'Repeats',
|
||
'detail.monthly': 'Every month',
|
||
'detail.yearly': 'Every year',
|
||
'detail.assignee': 'Assigned to',
|
||
'detail.subtasks': 'Sub-tasks',
|
||
'detail.addSubtask': 'Add a sub-task',
|
||
'detail.newSubtask': 'New sub-task',
|
||
'detail.files': 'Files',
|
||
'detail.delete': 'Delete',
|
||
'detail.reminders': 'Reminders',
|
||
'detail.newReminder': 'New reminder',
|
||
'detail.sent': 'sent',
|
||
'detail.add': 'Add',
|
||
'detail.comments': 'Comments',
|
||
'detail.writeComment': 'Write a comment',
|
||
'detail.newComment': 'New comment',
|
||
'detail.post': 'Post',
|
||
'detail.readAs': 'Read as {what}',
|
||
|
||
'auth.alreadySignedIn': 'You are signed in as {name}. Sign out first, then open the link again.',
|
||
|
||
'detail.reopen': 'Reopen',
|
||
'detail.uploading': 'Uploading …',
|
||
'detail.attachFile': 'Attach a file',
|
||
|
||
// --- settings: integrations --------------------------------------------------------
|
||
'int.mailboxes': 'Mailboxes',
|
||
'int.mailboxesHint':
|
||
'Flag an email in iCloud, Fastmail or any other provider and it becomes a task. One way: removing the flag does nothing to the task. Office 365 cannot be used here — Microsoft has closed IMAP to passwords.',
|
||
'int.addMailbox': 'Add a mailbox',
|
||
'int.host': 'IMAP server',
|
||
'int.username': 'Username',
|
||
'int.appPassword': 'App password',
|
||
'int.appPasswordHint':
|
||
'Not your ordinary password. iCloud and most others issue a separate one for applications, which you can withdraw on its own. It is stored encrypted.',
|
||
'int.connect': 'Connect',
|
||
'int.connecting': 'Connecting …',
|
||
'int.disconnect': 'Disconnect',
|
||
'int.disconnectMailbox': 'Disconnect {name}? The tasks it made will stay.',
|
||
'int.mailboxConnected': 'The mailbox is connected.',
|
||
'int.fetchNow': 'Fetch now',
|
||
'int.fetching': 'Fetching …',
|
||
'int.gmail': 'Gmail',
|
||
'int.gmailHint':
|
||
'Star an email, or give it a label, and it becomes a task. One way: removing the star does nothing to the task.',
|
||
'int.connectGmail': 'Connect Gmail',
|
||
'int.fromEnv':
|
||
'The client is set in the environment (VERDANDE_GMAIL_CLIENT_ID), so it cannot be changed here. That is deliberate: a value in the Rune’s manifest is a decision reapplied on every restart, and a form that could overwrite it would make the manifest a lie.',
|
||
'int.oauthClient': 'OAuth client at Google',
|
||
'int.registerHint':
|
||
'Register a client in Google Cloud Console → APIs & Services → Credentials → OAuth client ID → Web application, enable the Gmail API, and paste the two values here. Your own account has to be listed as a test user for as long as the app is unverified with Google.',
|
||
'int.redirectURI': 'Authorised redirect URI',
|
||
'int.clientID': 'Client ID',
|
||
'int.clientSecret': 'Client secret',
|
||
'int.secretStored': 'Stored — fill this in only to change it',
|
||
'int.save': 'Save',
|
||
'int.saved': 'Saved.',
|
||
'int.noClient':
|
||
'No OAuth client has been registered with Google yet. An administrator has to do that before this button can be used.',
|
||
'int.connectedAs': 'Connected as',
|
||
'int.unknownAccount': 'an unknown account',
|
||
'int.whatMakesATask': 'What makes a task',
|
||
'int.starred': 'Starred',
|
||
'int.aLabel': 'A particular label',
|
||
'int.both': 'Both',
|
||
'int.gmailLabelName': 'The label’s name in Gmail',
|
||
'int.gmailLabelExample': 'e.g. Tasks',
|
||
'int.fetchNow': 'Fetch now',
|
||
'int.disconnect': 'Disconnect',
|
||
'int.feed': 'Calendar feed',
|
||
'int.feedHint':
|
||
'Subscribe in Apple Calendar, Google or Thunderbird. A calendar client cannot sign in, so the key in the address is the whole password — do not share it.',
|
||
'int.address': 'Address',
|
||
'int.copy': 'Copy',
|
||
'int.newLink': 'New link',
|
||
'int.newLinkHint':
|
||
'A new link breaks every existing subscription immediately. That is the point: it is what you do when the old one has ended up somewhere it should not be.',
|
||
'int.mailToTask': 'Mail to task',
|
||
'int.mailHint':
|
||
'Send or forward an email here and the subject line is read as a quick add — “Invoice Anders p1 #Company” works here too.',
|
||
'int.noMailServer':
|
||
'No mail server is configured on this server, so the address below cannot receive anything yet. Set VERDANDE_SMTP_HOST and the other SMTP settings, and have the mail server deliver post for todo+* to /inbound/mail.',
|
||
'int.yourAddress': 'Your address',
|
||
'int.newAddress': 'New address',
|
||
'int.caldav': 'CalDAV',
|
||
'int.caldavHint':
|
||
'Two-way, unlike the feed above: Apple Reminders and Thunderbird can write back as well.',
|
||
'int.server': 'Server',
|
||
'int.caldavAuth':
|
||
'The username is your email. The password is an API token — not your real password.',
|
||
'int.gmailFetchedOne': '{n} task fetched.',
|
||
'int.gmailFetchedMany': '{n} tasks fetched.',
|
||
'int.gmailNothingNew': 'No new messages.',
|
||
'int.gmailConnected': 'Gmail is connected.',
|
||
'int.gmailState': 'Google’s reply did not match the attempt that started here. Try again.',
|
||
'int.gmailExpired': 'The attempt expired on the way. Try again.',
|
||
'int.gmailInvalid': 'Google’s reply could not be read. Try again.',
|
||
'int.gmailFailed': 'Google refused to exchange the code.',
|
||
'int.gmailNoRefresh':
|
||
'Google sent no refresh token. Remove verdande from your Google account and connect again.',
|
||
'int.gmailDenied': 'Access was refused at Google.',
|
||
'int.gmailOther': 'Gmail replied: {what}',
|
||
|
||
// --- a project ----------------------------------------------------------------------
|
||
'project.name': 'The project’s name',
|
||
'project.clickToRename': 'Click to rename',
|
||
'project.color': 'The project’s colour',
|
||
'project.pickColor': 'Pick a colour',
|
||
'project.delete': 'Delete the project',
|
||
'project.share': 'Share',
|
||
'project.history': 'History',
|
||
'project.noHistory': 'Nothing has happened here yet.',
|
||
'project.canEdit': 'Can edit',
|
||
'project.canView': 'Can only view',
|
||
'project.invite': 'Invite',
|
||
'project.inviteByEmail': 'Invite by email',
|
||
'project.emailAddress': 'email address',
|
||
'project.role': 'Role',
|
||
'project.sendLinkYourself': 'No mail server configured — send the link yourself:',
|
||
'project.noSection': 'No section',
|
||
'project.rename': 'Rename',
|
||
'project.deleteSection': 'Delete',
|
||
'project.readOnly': 'You can see this project, but not change it.',
|
||
'project.notFound': 'That project does not exist, or you do not have access to it.',
|
||
'project.loadFailed': 'The project could not be loaded. The server did not answer.',
|
||
'project.retry': 'Try again',
|
||
'project.nothingHere': 'Nothing here yet.',
|
||
|
||
'project.sharedWith': 'Shared · {n}',
|
||
|
||
// --- the palette ---------------------------------------------------------------
|
||
'color.graphite': 'Graphite',
|
||
'color.tomato': 'Tomato',
|
||
'color.amber': 'Amber',
|
||
'color.lime': 'Lime',
|
||
'color.green': 'Green',
|
||
'color.teal': 'Teal',
|
||
'color.blue': 'Blue',
|
||
'color.indigo': 'Indigo',
|
||
'color.violet': 'Violet',
|
||
'color.magenta': 'Magenta',
|
||
|
||
// --- what a failed request says -----------------------------------------------
|
||
'error.unauthorized': 'Wrong email or password.',
|
||
'error.totpRequired': 'Enter the code from your authenticator.',
|
||
'error.forbidden': 'You do not have access to that.',
|
||
'error.notFound': 'Not found.',
|
||
'error.conflict': 'That already exists.',
|
||
'error.rateLimited': 'Too many attempts. Wait a moment.',
|
||
'error.validation': 'Check the fields below.',
|
||
'error.tooLarge': 'That is too large.',
|
||
'error.internal': 'Something went wrong. Try again.',
|
||
'error.badRequest': 'The request could not be read.',
|
||
'error.offline': 'No connection to the server.',
|
||
'error.gmailNotConfigured':
|
||
'Gmail is not set up on this server. An administrator has to register an OAuth client with Google under Settings → Integrations.',
|
||
'error.aiNotConfigured': 'No AI provider has been chosen. Set one up under Settings → AI.',
|
||
'error.totpNotEnabled': 'Two-factor is not on.',
|
||
'error.inboxProtected': 'The Inbox cannot be deleted.',
|
||
'error.lastAdmin':
|
||
'That is the last administrator. Make somebody else an administrator first — otherwise nobody can get back into this page.',
|
||
|
||
// --- the themes -------------------------------------------------------------------
|
||
'theme.dark': 'Northern lights',
|
||
'theme.darkNote': 'Dark, with a green accent.',
|
||
'theme.dusk': 'Dusk',
|
||
'theme.duskNote': 'Dark and warm, with amber.',
|
||
'theme.light': 'Daylight',
|
||
'theme.lightNote': 'Light and coolly neutral.',
|
||
'theme.paper': 'Paper',
|
||
'theme.paperNote': 'Light and warm, like paper.',
|
||
'theme.contrast': 'Contrast',
|
||
'theme.contrastNote': 'Black on white, for bright light.',
|
||
|
||
// --- push, in the browser's own words ----------------------------------------------
|
||
'push.deniedByBrowser':
|
||
'The browser has blocked notifications for this site. It has to be turned on in the browser’s own settings.',
|
||
'push.notEnabled': 'Notifications were not turned on.',
|
||
'push.noVapidKey': 'The server has no VAPID key.',
|
||
|
||
'view.done': 'Done',
|
||
'view.showDone': 'Show done',
|
||
'view.hideDone': 'Hide done',
|
||
|
||
'task.moveFailed': 'The task could not be moved.',
|
||
|
||
'push.updatesOff': 'Updates are not being checked for.',
|
||
'push.updateOut': '{version} is out.',
|
||
'push.upToDate': 'No newer version found.',
|
||
|
||
'view.nothingMoreToday': 'Nothing more today.',
|
||
'project.roleFor': 'Role for {name}',
|
||
|
||
'project.more': 'More actions',
|
||
'project.removeMember': 'Remove {name}',
|
||
|
||
'nav.closeMenu': 'Close menu',
|
||
|
||
'view.addTask': '+ Add a task',
|
||
'view.addTaskHere': 'Write a task — a date, p1 and @label work here too',
|
||
'view.addTaskIn': 'New task in {name}',
|
||
|
||
'task.completedUndo': '“{what}” is done.',
|
||
'task.undo': 'Undo',
|
||
|
||
'done.title': 'Done',
|
||
'done.hint': 'Everything you have closed, newest first. Click the circle to reopen one.',
|
||
'done.empty': 'You have not closed anything yet.',
|
||
'done.unknownDay': 'No date',
|
||
|
||
// --- passkeys ----------------------------------------------------------------------
|
||
'passkey.title': 'Passkeys',
|
||
'passkey.hint':
|
||
'A key on a device instead of a secret in a head. The private half never leaves the device, and the signature is bound to the address that asked for it — so a convincing copy of the sign-in page somewhere else gets nothing.',
|
||
'passkey.unsupported': 'This browser does not support passkeys.',
|
||
'passkey.none': 'No keys yet.',
|
||
'passkey.name': 'Name for the key',
|
||
'passkey.namePlaceholder': 'e.g. my laptop',
|
||
'passkey.add': 'Add a key',
|
||
'passkey.registering': 'Waiting for the device…',
|
||
'passkey.remove': 'Remove',
|
||
'passkey.removeQuestion': 'Remove "{name}"? That device cannot sign in with it afterwards.',
|
||
'passkey.bothFactors': 'both factors',
|
||
'passkey.possessionOnly': 'possession only — the code is still required',
|
||
'passkey.lastUsed': 'last used {when}',
|
||
'passkey.neverUsed': 'never used',
|
||
'passkey.signIn': 'Sign in with a passkey',
|
||
'passkey.signingIn': 'Waiting for the device…',
|
||
|
||
'passkey.unavailable':
|
||
'Passkeys cannot be used here — either this browser does not support them, or the server’s address is an IP rather than a domain name, which no authenticator will accept.',
|
||
|
||
// --- restarting from the interface ---------------------------------------------
|
||
'update.restart': 'Restart and pull the latest version',
|
||
'update.restarting': 'Restarting…',
|
||
'update.restartQuestion':
|
||
'Restart now? The server is down for about half a minute, and pulls the newest version on the way back up.',
|
||
'update.asking': 'Asking the panel to restart…',
|
||
'update.waiting': 'Waiting for the server to come back…',
|
||
'update.stillDown': 'The server has not come back yet. Have a look in the panel.',
|
||
'update.notConfigured':
|
||
'This instance cannot restart itself: {missing} are not set. A container cannot replace its own image — the panel does the work, and this is only the way to ask.',
|
||
|
||
'error.gmailSaid': 'Gmail said: {what}',
|
||
|
||
'error.mailboxRefused': 'The mailbox could not be read.',
|
||
'error.mailboxSaid': 'The mailbox answered: {what}',
|
||
'error.gmailRefused': 'Gmail refused the call. Try connecting the mailbox again.',
|
||
|
||
'int.newAddressQuestion': 'A new address? Mail to the old one is refused.',
|
||
'int.copied': 'Copied.',
|
||
'int.copyFailed': 'Could not copy — select the text instead.',
|
||
|
||
'detail.deleteComment': 'Delete the comment?',
|
||
'int.disconnectQuestion': 'Disconnect Gmail? The tokens are forgotten.',
|
||
'int.newLinkQuestion': 'A new feed link? Every existing subscription stops working.',
|
||
'data.deleteTemplate': 'Delete the template "{name}"?',
|
||
'tokens.revokeQuestion': 'Revoke "{name}"? Everything using it stops working.',
|
||
'account.signOutThisDevice': 'That is this device. You will be signed out.',
|
||
|
||
'project.deleteQuestion': 'Delete "{name}" with all its tasks?',
|
||
'project.deleteRecoverable':
|
||
'It goes to the trash and can be restored under Settings → Data and templates for {days} days.'
|
||
};
|