forked from vikunja/frontend
Compare commits
49 Commits
main
...
translatio
Author | SHA1 | Date |
---|---|---|
Andrey Kashlak | 9800e1701d | |
Andrey Kashlak | 8e6633f70f | |
Andrey Kashlak | eaca985d44 | |
Andrey Kashlak | 1d5def2d8e | |
Andrey Kashlak | 7d5077cd8f | |
Jesse James Isler | b24365640f | |
J. Lavoie | 024af54cd1 | |
Andrey Kashlak | 5cf1fb831a | |
J. Lavoie | eb6ade1fac | |
J. Lavoie | 49fa8dd5ff | |
Allan Nordhøy | 004484fbd7 | |
Luis | 86e9cfdf4b | |
Allan Nordhøy | 32dd9bf138 | |
Allan Nordhøy | 7476949852 | |
Allan Nordhøy | f7e24f9df3 | |
kolaente | 82b756cd99 | |
Allan Nordhøy | 6ade8c6607 | |
Allan Nordhøy | da71cf7220 | |
Allan Nordhøy | 4a7d0d5b7b | |
Allan Nordhøy | 76f67f60bc | |
Allan Nordhøy | 812d1ba560 | |
Allan Nordhøy | aef4792be5 | |
kolaente | e2959f210d | |
kolaente | 33ff902c6c | |
kolaente | fca4b93002 | |
Luis | dc41288ec1 | |
Anonymous | 2fd47b585d | |
Anonymous | 44a4e08d0d | |
Anonymous | db31574858 | |
Anonymous | 345f02b66a | |
Anonymous | 53a4e463f2 | |
Konrad | d3586a3d5c | |
kolaente | 3aa8488dc4 | |
Swann Fournial | b0827e2ba8 | |
Swann Fournial | 4123d739d9 | |
Andrey Kashlak | d55fdbf223 | |
Andrey Kashlak | 2b8884c39a | |
Andrey Kashlak | b93d853022 | |
Andrey Kashlak | 3db06bc81b | |
Andrey Kashlak | 3416c2598e | |
Andrey Kashlak | 2d754f0aac | |
Swann Fournial | 01669831e5 | |
Andrey Kashlak | b25cea2180 | |
Swann Fournial | be86427374 | |
Konrad | 4dbec1acab | |
kolaente | e096de57d3 | |
Swann Fournial | 4ba6261549 | |
Nathan | a707931c55 | |
Andrey Kashlak | 44bdbd2fdb |
|
@ -1,5 +1,5 @@
|
|||
# Stage 1: Build application
|
||||
FROM node:16 AS compile-image
|
||||
FROM node:16.3.0 AS compile-image
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
|
|
|
@ -511,34 +511,4 @@ describe('Lists', () => {
|
|||
.should('not.contain', task.title)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List history', () => {
|
||||
it('should show a list history on the home page', () => {
|
||||
const lists = ListFactory.create(6)
|
||||
|
||||
cy.visit('/')
|
||||
cy.get('h3')
|
||||
.contains('Last viewed')
|
||||
.should('not.exist')
|
||||
|
||||
cy.visit(`/lists/${lists[0].id}`)
|
||||
cy.visit(`/lists/${lists[1].id}`)
|
||||
cy.visit(`/lists/${lists[2].id}`)
|
||||
cy.visit(`/lists/${lists[3].id}`)
|
||||
cy.visit(`/lists/${lists[4].id}`)
|
||||
cy.visit(`/lists/${lists[5].id}`)
|
||||
|
||||
cy.visit('/')
|
||||
cy.get('h3')
|
||||
.contains('Last viewed')
|
||||
.should('exist')
|
||||
cy.get('.list-cards-wrapper-2-rows')
|
||||
.should('not.contain', lists[0].title)
|
||||
.should('contain', lists[1].title)
|
||||
.should('contain', lists[2].title)
|
||||
.should('contain', lists[3].title)
|
||||
.should('contain', lists[4].title)
|
||||
.should('contain', lists[5].title)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
import '../../support/authenticateUser'
|
||||
|
||||
const setHours = hours => {
|
||||
const date = new Date()
|
||||
date.setHours(hours)
|
||||
cy.clock(+date)
|
||||
}
|
||||
|
||||
describe('Home Page', () => {
|
||||
it('shows the right salutation in the night', () => {
|
||||
setHours(4)
|
||||
cy.visit('/')
|
||||
cy.get('h2').should('contain', 'Good Night')
|
||||
})
|
||||
it('shows the right salutation in the morning', () => {
|
||||
setHours(8)
|
||||
cy.visit('/')
|
||||
cy.get('h2').should('contain', 'Good Morning')
|
||||
})
|
||||
it('shows the right salutation in the day', () => {
|
||||
setHours(13)
|
||||
cy.visit('/')
|
||||
cy.get('h2').should('contain', 'Hi')
|
||||
})
|
||||
it('shows the right salutation in the night', () => {
|
||||
setHours(20)
|
||||
cy.visit('/')
|
||||
cy.get('h2').should('contain', 'Good Evening')
|
||||
})
|
||||
it('shows the right salutation in the night again', () => {
|
||||
setHours(23)
|
||||
cy.visit('/')
|
||||
cy.get('h2').should('contain', 'Good Night')
|
||||
})
|
||||
})
|
|
@ -34,7 +34,6 @@ context('Login', () => {
|
|||
cy.get('input[id=password]').type(fixture.password)
|
||||
cy.get('.button').contains('Login').click()
|
||||
cy.url().should('include', '/')
|
||||
cy.clock(1625656161057) // 13:00
|
||||
cy.get('h2').should('contain', `Hi ${fixture.username}!`)
|
||||
})
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ context('Registration', () => {
|
|||
cy.get('#password2').type(fixture.password)
|
||||
cy.get('#register-submit').click()
|
||||
cy.url().should('include', '/')
|
||||
cy.clock(1625656161057) // 13:00
|
||||
cy.get('h2').should('contain', `Hi ${fixture.username}!`)
|
||||
})
|
||||
|
||||
|
|
17
package.json
17
package.json
|
@ -18,12 +18,12 @@
|
|||
"camel-case": "4.1.2",
|
||||
"copy-to-clipboard": "3.3.1",
|
||||
"date-fns": "2.22.1",
|
||||
"dompurify": "2.3.0",
|
||||
"dompurify": "2.2.9",
|
||||
"highlight.js": "11.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "2.1.3",
|
||||
"register-service-worker": "1.7.2",
|
||||
"sass": "1.35.2",
|
||||
"sass": "1.35.1",
|
||||
"snake-case": "3.0.4",
|
||||
"verte": "0.0.12",
|
||||
"vue": "2.6.14",
|
||||
|
@ -47,14 +47,14 @@
|
|||
"@vue/cli-service": "4.5.13",
|
||||
"axios": "0.21.1",
|
||||
"babel-eslint": "10.1.0",
|
||||
"cypress": "7.7.0",
|
||||
"cypress": "7.6.0",
|
||||
"cypress-file-upload": "5.0.8",
|
||||
"eslint": "7.30.0",
|
||||
"eslint-plugin-vue": "7.13.0",
|
||||
"eslint": "7.29.0",
|
||||
"eslint-plugin-vue": "7.12.1",
|
||||
"faker": "5.5.3",
|
||||
"jest": "27.0.6",
|
||||
"jest": "27.0.5",
|
||||
"sass-loader": "10.2.0",
|
||||
"vue-flatpickr-component": "8.1.7",
|
||||
"vue-flatpickr-component": "8.1.6",
|
||||
"vue-notification": "1.3.20",
|
||||
"vue-router": "3.5.2",
|
||||
"vue-template-compiler": "2.6.14",
|
||||
|
@ -92,7 +92,6 @@
|
|||
"jest": {
|
||||
"testPathIgnorePatterns": [
|
||||
"cypress"
|
||||
],
|
||||
"testEnvironment": "jsdom"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,14 +54,14 @@
|
|||
<span
|
||||
@click="toggleLists(n.id)"
|
||||
class="menu-label"
|
||||
v-tooltip="getNamespaceTitle(n) + ' (' + n.lists.filter(l => !l.isArchived).length + ')'">
|
||||
v-tooltip="n.title + ' (' + n.lists.filter(l => !l.isArchived).length + ')'">
|
||||
<span class="name">
|
||||
<span
|
||||
:style="{ backgroundColor: n.hexColor }"
|
||||
class="color-bubble"
|
||||
v-if="n.hexColor !== ''">
|
||||
</span>
|
||||
{{ getNamespaceTitle(n) }} ({{ n.lists.filter(l => !l.isArchived).length }})
|
||||
{{ n.title }} ({{ n.lists.filter(l => !l.isArchived).length }})
|
||||
</span>
|
||||
</span>
|
||||
<a
|
||||
|
@ -91,7 +91,7 @@
|
|||
v-if="l.hexColor !== ''">
|
||||
</span>
|
||||
<span class="list-menu-title">
|
||||
{{ getListTitle(l) }}
|
||||
{{ l.title }}
|
||||
</span>
|
||||
<span
|
||||
:class="{'is-favorite': l.isFavorite}"
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<h1
|
||||
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"
|
||||
class="title">
|
||||
{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}
|
||||
{{ currentList.title === '' ? $t('misc.loading') : currentList.title }}
|
||||
</h1>
|
||||
|
||||
<list-settings-dropdown v-if="canWriteCurrentList && currentList.id !== -1" :list="currentList"/>
|
||||
|
@ -82,9 +82,6 @@
|
|||
<a @click="$store.commit('keyboardShortcutsActive', true)" class="dropdown-item">
|
||||
{{ $t('keyboardShortcuts.title') }}
|
||||
</a>
|
||||
<router-link :to="{name: 'about'}" class="dropdown-item">
|
||||
{{ $t('about.title') }}
|
||||
</router-link>
|
||||
<a @click="logout()" class="dropdown-item">
|
||||
{{ $t('user.auth.logout') }}
|
||||
</a>
|
||||
|
|
|
@ -35,15 +35,15 @@
|
|||
<p class="has-text-centered has-text-grey is-italic" v-if="isPreviewActive && text === '' && emptyText !== ''">
|
||||
{{ emptyText }}
|
||||
<template v-if="isEditEnabled">
|
||||
<a @click="toggleEdit">{{ $t('input.editor.edit') }}</a>.
|
||||
<a @click="toggleEdit">Edit</a>.
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<ul class="actions">
|
||||
<template v-if="hasEditBottom && isEditEnabled">
|
||||
<li>
|
||||
<a v-if="!isEditActive" @click="toggleEdit">{{ $t('input.editor.edit') }}</a>
|
||||
<a v-else @click="toggleEdit">{{ $t('input.editor.done') }}</a>
|
||||
<a v-if="!isEditActive" @click="toggleEdit">Edit</a>
|
||||
<a v-else @click="toggleEdit">Done</a>
|
||||
</li>
|
||||
</template>
|
||||
<li v-for="(action, k) in bottomActions" :key="k">
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
<template>
|
||||
<router-link
|
||||
:class="{
|
||||
'has-light-text': !colorIsDark(list.hexColor),
|
||||
'has-background': background !== null
|
||||
}"
|
||||
:style="{
|
||||
'background-color': list.hexColor,
|
||||
'background-image': background !== null ? `url(${background})` : false,
|
||||
}"
|
||||
:to="{ name: 'list.index', params: { listId: list.id} }"
|
||||
class="list-card"
|
||||
tag="span"
|
||||
v-if="list !== null && (showArchived ? true : !list.isArchived)"
|
||||
>
|
||||
<div class="is-archived-container">
|
||||
<span class="is-archived" v-if="list.isArchived">
|
||||
{{ $t('namespace.archived') }}
|
||||
</span>
|
||||
<span
|
||||
:class="{'is-favorite': list.isFavorite, 'is-archived': list.isArchived}"
|
||||
@click.stop="toggleFavoriteList(list)"
|
||||
class="favorite">
|
||||
<icon icon="star" v-if="list.isFavorite"/>
|
||||
<icon :icon="['far', 'star']" v-else/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="title">{{ list.title }}</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListService from '@/services/list'
|
||||
|
||||
export default {
|
||||
name: 'list-card',
|
||||
data() {
|
||||
return {
|
||||
background: null,
|
||||
backgroundLoading: false,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
list: {
|
||||
required: true,
|
||||
},
|
||||
showArchived: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
list() {
|
||||
this.loadBackground()
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadBackground()
|
||||
},
|
||||
methods: {
|
||||
loadBackground() {
|
||||
if (this.list === null || !this.list.backgroundInformation || this.backgroundLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
this.backgroundLoading = true
|
||||
|
||||
const listService = new ListService()
|
||||
listService.background(this.list)
|
||||
.then(b => {
|
||||
this.$set(this, 'background', b)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e)
|
||||
})
|
||||
.finally(() => this.backgroundLoading = false)
|
||||
},
|
||||
toggleFavoriteList(list) {
|
||||
// The favorites pseudo list is always favorite
|
||||
// Archived lists cannot be marked favorite
|
||||
if (list.id === -1 || list.isArchived) {
|
||||
return
|
||||
}
|
||||
this.$store.dispatch('lists/toggleListFavorite', list)
|
||||
.catch(e => this.error(e))
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="modal-mask hint-modal">
|
||||
<div class="modal-mask keyboard-shortcuts-modal">
|
||||
<div @click.self="close()" class="modal-container">
|
||||
<div class="modal-content">
|
||||
<card class="has-background-white has-no-shadow" :title="$t('keyboardShortcuts.title')">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<transition name="modal">
|
||||
<div class="modal-mask has-overflow" :class="{'has-overflow': overflow}">
|
||||
<div class="modal-container" @mousedown.self.prevent.stop="$emit('close')" :class="{'has-overflow': overflow}">
|
||||
<div class="modal-mask">
|
||||
<div class="modal-container" @mousedown.self.prevent.stop="$emit('close')">
|
||||
<div class="modal-content" :class="{'has-overflow': overflow, 'is-wide': wide}">
|
||||
<slot>
|
||||
<div class="header">
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<transition name="fade">
|
||||
<div class="notifications-list" v-if="showNotifications" ref="popup">
|
||||
<span class="head">{{ $t('notification.title') }}</span>
|
||||
<span class="head">Notifications</span>
|
||||
<div
|
||||
v-for="(n, index) in notifications"
|
||||
:key="n.id"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<modal v-if="active" class="quick-actions" @close="closeQuickActions" :overflow="isNewTaskCommand">
|
||||
<modal v-if="active" class="quick-actions" @close="closeQuickActions">
|
||||
<div class="card">
|
||||
<div class="action-input" :class="{'has-active-cmd': selectedCmd !== null}">
|
||||
<div class="active-cmd tag" v-if="selectedCmd !== null">
|
||||
|
@ -20,12 +20,10 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div class="help has-text-grey-light p-2" v-if="hintText !== '' && !isNewTaskCommand">
|
||||
<div class="help has-text-grey-light p-2" v-if="hintText !== ''">
|
||||
{{ hintText }}
|
||||
</div>
|
||||
|
||||
<quick-add-magic class="p-2 modal-container-smaller" v-if="isNewTaskCommand"/>
|
||||
|
||||
<div class="results" v-if="selectedCmd === null">
|
||||
<div v-for="(r, k) in results" :key="k" class="result">
|
||||
<span class="result-title">
|
||||
|
@ -41,7 +39,6 @@
|
|||
@click.prevent.stop="() => doAction(r.type, i)"
|
||||
@keyup.prevent.enter="() => doAction(r.type, i)"
|
||||
@keyup.prevent.esc="() => $refs.searchInput.focus()"
|
||||
:class="{'is-strikethrough': i.done}"
|
||||
>
|
||||
{{ i.title }}
|
||||
</button>
|
||||
|
@ -56,14 +53,12 @@
|
|||
import TaskService from '@/services/task'
|
||||
import TeamService from '@/services/team'
|
||||
|
||||
import TaskModel from '@/models/task'
|
||||
import NamespaceModel from '@/models/namespace'
|
||||
import TeamModel from '@/models/team'
|
||||
|
||||
import {CURRENT_LIST, LOADING, LOADING_MODULE, QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
|
||||
import ListModel from '@/models/list'
|
||||
import createTask from '@/components/tasks/mixins/createTask'
|
||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic'
|
||||
import {getHistory} from '@/modules/listHistory'
|
||||
|
||||
const TYPE_LIST = 'list'
|
||||
const TYPE_TASK = 'task'
|
||||
|
@ -82,7 +77,6 @@ const SEARCH_MODE_TEAMS = 'teams'
|
|||
|
||||
export default {
|
||||
name: 'quick-actions',
|
||||
components: {QuickAddMagic},
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
|
@ -97,9 +91,6 @@ export default {
|
|||
teamService: null,
|
||||
}
|
||||
},
|
||||
mixins: [
|
||||
createTask,
|
||||
],
|
||||
computed: {
|
||||
active() {
|
||||
const active = this.$store.state[QUICK_ACTIONS_ACTIVE]
|
||||
|
@ -116,29 +107,7 @@ export default {
|
|||
query = query.substr(1)
|
||||
}
|
||||
|
||||
const ncache = {}
|
||||
|
||||
const history = getHistory()
|
||||
// Puts recently visited lists at the top
|
||||
const allLists = [...new Set([
|
||||
...history.map(l => {
|
||||
return this.$store.getters['lists/getListById'](l.id)
|
||||
}),
|
||||
...Object.values(this.$store.state.lists)])]
|
||||
|
||||
lists = (allLists.filter(l => {
|
||||
if (l.isArchived) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof ncache[l.namespaceId] === 'undefined') {
|
||||
ncache[l.namespaceId] = this.$store.getters['namespaces/getNamespaceById'](l.namespaceId)
|
||||
}
|
||||
|
||||
if (ncache[l.namespaceId].isArchived) {
|
||||
return false
|
||||
}
|
||||
|
||||
lists = (Object.values(this.$store.state.lists).filter(l => {
|
||||
return l.title.toLowerCase().includes(query.toLowerCase())
|
||||
}) ?? [])
|
||||
}
|
||||
|
@ -253,9 +222,6 @@ export default {
|
|||
|
||||
return SEARCH_MODE_ALL
|
||||
},
|
||||
isNewTaskCommand() {
|
||||
return this.selectedCmd !== null && this.selectedCmd.action === CMD_NEW_TASK
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.taskService = new TaskService()
|
||||
|
@ -382,7 +348,11 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
this.createNewTask(this.query, 0, this.currentList.id)
|
||||
const newTask = new TaskModel({
|
||||
title: this.query,
|
||||
listId: this.currentList.id,
|
||||
})
|
||||
this.taskService.create(newTask)
|
||||
.then(r => {
|
||||
this.success({message: this.$t('task.createSuccess')})
|
||||
this.$router.push({name: 'task.detail', params: {id: r.id}})
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
id="linkSharePassword"
|
||||
type="password"
|
||||
class="input"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
:placeholder="$t('user.auth.passwortPlaceholder')"
|
||||
v-tooltip="$t('list.share.links.passwordExplanation')"
|
||||
v-model="password"
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import AttachmentModel from '../../../models/attachment'
|
||||
import AttachmentService from '../../../services/attachment'
|
||||
import {generateAttachmentUrl} from '@/helpers/generateAttachmentUrl'
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
|
@ -22,7 +21,7 @@ export default {
|
|||
taskId: this.taskId,
|
||||
attachment: a,
|
||||
})
|
||||
onSuccess(generateAttachmentUrl(this.taskId, a.id))
|
||||
onSuccess(`${window.API_URL}/tasks/${this.taskId}/attachments/${a.id}`)
|
||||
})
|
||||
}
|
||||
if (r.errors !== null) {
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
import {parseTaskText} from '@/helpers/parseTaskText'
|
||||
import TaskModel from '@/models/task'
|
||||
import {formatISO} from 'date-fns'
|
||||
import LabelTask from '@/models/labelTask'
|
||||
import LabelModel from '@/models/label'
|
||||
import LabelTaskService from '@/services/labelTask'
|
||||
import {mapState} from 'vuex'
|
||||
import UserService from '@/services/user'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
labelTaskService: LabelTaskService,
|
||||
userService: UserService,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.labelTaskService = new LabelTaskService()
|
||||
this.userService = new UserService()
|
||||
},
|
||||
computed: mapState({
|
||||
labels: state => state.labels.labels,
|
||||
}),
|
||||
methods: {
|
||||
createNewTask(newTaskTitle, bucketId = 0, lId = 0) {
|
||||
const parsedTask = parseTaskText(newTaskTitle)
|
||||
const assignees = []
|
||||
|
||||
let listId = null
|
||||
if (parsedTask.list !== null) {
|
||||
const list = this.$store.getters['lists/findListByExactname'](parsedTask.list)
|
||||
listId = list === null ? null : list.id
|
||||
}
|
||||
if (listId === null) {
|
||||
listId = lId !== 0 ? lId : this.$route.params.listId
|
||||
}
|
||||
|
||||
// Separate closure because we need to wait for the results of the user search if users were entered in the
|
||||
// task create request. Because _that_ happens in a promise, we'll need something to call when it resolves.
|
||||
const createTask = () => {
|
||||
const task = new TaskModel({
|
||||
title: parsedTask.text,
|
||||
listId: listId,
|
||||
dueDate: parsedTask.date !== null ? formatISO(parsedTask.date) : null, // I don't know why, but it all goes up in flames when I just pass in the date normally.
|
||||
priority: parsedTask.priority,
|
||||
assignees: assignees,
|
||||
bucketId: bucketId,
|
||||
})
|
||||
return this.taskService.create(task)
|
||||
.then(task => {
|
||||
|
||||
if (parsedTask.labels.length > 0) {
|
||||
|
||||
const labelAddsToWaitFor = []
|
||||
|
||||
const addLabelToTask = label => {
|
||||
const labelTask = new LabelTask({
|
||||
taskId: task.id,
|
||||
labelId: label.id,
|
||||
})
|
||||
return this.labelTaskService.create(labelTask)
|
||||
.then(result => {
|
||||
task.labels.push(label)
|
||||
return Promise.resolve(result)
|
||||
})
|
||||
.catch(e => Promise.reject(e))
|
||||
}
|
||||
|
||||
// Then do everything that is involved in finding, creating and adding the label to the task
|
||||
parsedTask.labels.forEach(labelTitle => {
|
||||
// Check if the label exists
|
||||
const label = Object.values(this.labels).find(l => {
|
||||
return l.title.toLowerCase() === labelTitle.toLowerCase()
|
||||
})
|
||||
|
||||
// Label found, use it
|
||||
if (typeof label !== 'undefined') {
|
||||
labelAddsToWaitFor.push(addLabelToTask(label))
|
||||
} else {
|
||||
// label not found, create it
|
||||
const label = new LabelModel({title: labelTitle})
|
||||
labelAddsToWaitFor.push(this.$store.dispatch('labels/createLabel', label)
|
||||
.then(res => {
|
||||
return addLabelToTask(res)
|
||||
})
|
||||
.catch(e => Promise.reject(e))
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// This waits until all labels are created and added to the task
|
||||
return Promise.all(labelAddsToWaitFor)
|
||||
.then(() => {
|
||||
return Promise.resolve(task)
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve(task)
|
||||
})
|
||||
.catch(e => Promise.reject(e))
|
||||
}
|
||||
|
||||
if (parsedTask.assignees.length > 0) {
|
||||
const searches = []
|
||||
parsedTask.assignees.forEach(a => {
|
||||
searches.push(this.userService.getAll({}, {s: a})
|
||||
.then(users => {
|
||||
const user = users.find(u => u.username.toLowerCase() === a.toLowerCase())
|
||||
if (typeof user !== 'undefined') {
|
||||
assignees.push(user)
|
||||
}
|
||||
return Promise.resolve(users)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
return Promise.all(searches)
|
||||
.then(() => createTask())
|
||||
}
|
||||
|
||||
return createTask()
|
||||
},
|
||||
},
|
||||
}
|
|
@ -55,20 +55,14 @@
|
|||
<p>
|
||||
<a
|
||||
@click.prevent.stop="downloadAttachment(a)"
|
||||
v-tooltip="$t('task.attachment.downloadTooltip')"
|
||||
v-tooltip="'Download this attachment'"
|
||||
>
|
||||
{{ $t('task.attachment.download') }}
|
||||
</a>
|
||||
<a
|
||||
@click.stop="copyUrl(a)"
|
||||
v-tooltip="$t('task.attachment.copyUrlTooltip')"
|
||||
>
|
||||
{{ $t('task.attachment.copyUrl') }}
|
||||
</a>
|
||||
<a
|
||||
@click.prevent.stop="() => {attachmentToDelete = a; showDeleteModal = true}"
|
||||
v-if="editEnabled"
|
||||
v-tooltip="$t('task.attachment.deleteTooltip')"
|
||||
v-tooltip="'Delete this attachment'"
|
||||
>
|
||||
{{ $t('misc.delete') }}
|
||||
</a>
|
||||
|
@ -112,7 +106,7 @@
|
|||
>
|
||||
<span slot="header">{{ $t('task.attachment.delete') }}</span>
|
||||
<p slot="text">
|
||||
{{ $t('task.attachment.deleteText1', {filename: attachmentToDelete.file.name}) }}<br/>
|
||||
{{ $t('task.attachment.deleteText1', {filename: attachmentUpload.file.name}) }}<br/>
|
||||
<strong>{{ $t('task.attachment.deleteText2') }}</strong>
|
||||
</p>
|
||||
</modal>
|
||||
|
@ -139,9 +133,7 @@ import AttachmentService from '../../../services/attachment'
|
|||
import AttachmentModel from '../../../models/attachment'
|
||||
import User from '../../misc/user'
|
||||
import attachmentUpload from '@/components/tasks/mixins/attachmentUpload'
|
||||
import {generateAttachmentUrl} from '@/helpers/generateAttachmentUrl'
|
||||
import {mapState} from 'vuex'
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
export default {
|
||||
name: 'attachments',
|
||||
|
@ -255,9 +247,6 @@ export default {
|
|||
this.downloadAttachment(attachment)
|
||||
}
|
||||
},
|
||||
copyUrl(attachment) {
|
||||
copy(generateAttachmentUrl(this.taskId, attachment.id))
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<p class="help has-text-grey">
|
||||
{{ $t('task.quickAddMagic.hint') }}.
|
||||
<a @click="() => visible = true">{{ $t('task.quickAddMagic.what') }}</a>
|
||||
</p>
|
||||
<transition name="fade">
|
||||
<div class="modal-mask hint-modal" v-if="visible">
|
||||
<div @click.self="() => visible = false" class="modal-container">
|
||||
<div class="modal-content">
|
||||
<card class="has-background-white has-no-shadow" :title="$t('task.quickAddMagic.title')">
|
||||
<p>{{ $t('task.quickAddMagic.intro') }}</p>
|
||||
|
||||
<h3>{{ $t('task.attributes.labels') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.label1', {prefix: '@'}) }}
|
||||
{{ $t('task.quickAddMagic.label2') }}
|
||||
{{ $t('task.quickAddMagic.multiple') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.label3') }}
|
||||
{{ $t('task.quickAddMagic.label4', {prefix: '@'}) }}
|
||||
</p>
|
||||
|
||||
<h3>{{ $t('task.attributes.priority') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.priority1', {prefix: '!'}) }}
|
||||
{{ $t('task.quickAddMagic.priority2') }}
|
||||
</p>
|
||||
|
||||
<h3>{{ $t('task.attributes.assignees') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.assignees') }}
|
||||
{{ $t('task.quickAddMagic.multiple') }}
|
||||
</p>
|
||||
|
||||
<h3>{{ $t('list.list.title') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.list1', {prefix: '#'}) }}
|
||||
{{ $t('task.quickAddMagic.list2') }}
|
||||
</p>
|
||||
|
||||
<h3>{{ $t('task.quickAddMagic.dateAndTime') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.date') }}
|
||||
</p>
|
||||
<ul>
|
||||
<!-- Not localized because these only work in english -->
|
||||
<li>Today</li>
|
||||
<li>Tomorrow</li>
|
||||
<li>Next monday</li>
|
||||
<li>This weekend</li>
|
||||
<li>Later this week</li>
|
||||
<li>Later next week</li>
|
||||
<li>Next week</li>
|
||||
<li>Next month</li>
|
||||
<li>End of month</li>
|
||||
<li>In 5 days [hours/weeks/months]</li>
|
||||
<li>Tuesday ({{ $t('task.quickAddMagic.dateWeekday') }})</li>
|
||||
<li>17/02/2021</li>
|
||||
<li>Feb 17 ({{ $t('task.quickAddMagic.dateCurrentYear') }})</li>
|
||||
<li>17th ({{ $t('task.quickAddMagic.dateNth', {day: '17'}) }})</li>
|
||||
</ul>
|
||||
<p>{{ $t('task.quickAddMagic.dateTime', {time: 'at 17:00', timePM: '5pm'}) }}</p>
|
||||
</card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'quick-add-magic',
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,8 +1,4 @@
|
|||
export const colorIsDark = color => {
|
||||
if (typeof color === 'undefined') {
|
||||
return true // Defaults to dark
|
||||
}
|
||||
|
||||
if (color === '#' || color === '') {
|
||||
return true // Defaults to dark
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export const generateAttachmentUrl = (taskId, attachmentId) => {
|
||||
return `${window.API_URL}/tasks/${taskId}/attachments/${attachmentId}`
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export const getListTitle = (l, $t) => {
|
||||
if (l.id === -1) {
|
||||
return $t('list.pseudo.favorites.title');
|
||||
}
|
||||
return l.title;
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
export const getNamespaceTitle = (n, $t) => {
|
||||
if (n.id === -1) {
|
||||
return $t('namespace.pseudo.sharedLists.title');
|
||||
}
|
||||
if (n.id === -2) {
|
||||
return $t('namespace.pseudo.favorites.title');
|
||||
}
|
||||
if (n.id === -3) {
|
||||
return $t('namespace.pseudo.savedFilters.title');
|
||||
}
|
||||
return n.title;
|
||||
}
|
|
@ -1,8 +1,4 @@
|
|||
export const parseDateOrNull = date => {
|
||||
if (date instanceof Date) {
|
||||
return date
|
||||
}
|
||||
|
||||
if (date && !date.startsWith('0001')) {
|
||||
return new Date(date)
|
||||
}
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
import {parseDate} from './time/parseDate'
|
||||
import priorities from '../models/priorities.json'
|
||||
|
||||
const LABEL_PREFIX = '@'
|
||||
const LIST_PREFIX = '#'
|
||||
const PRIORITY_PREFIX = '!'
|
||||
const ASSIGNEE_PREFIX = '+'
|
||||
|
||||
/**
|
||||
* Parses task text for dates, assignees, labels, lists, priorities and returns an object with all found intents.
|
||||
*
|
||||
* @param text
|
||||
*/
|
||||
export const parseTaskText = text => {
|
||||
const result = {
|
||||
text: text,
|
||||
date: null,
|
||||
labels: [],
|
||||
list: null,
|
||||
priority: null,
|
||||
assignees: [],
|
||||
}
|
||||
|
||||
result.labels = getItemsFromPrefix(text, LABEL_PREFIX)
|
||||
|
||||
const lists = getItemsFromPrefix(text, LIST_PREFIX)
|
||||
result.list = lists.length > 0 ? lists[0] : null
|
||||
|
||||
result.priority = getPriority(text)
|
||||
|
||||
result.assignees = getItemsFromPrefix(text, ASSIGNEE_PREFIX)
|
||||
|
||||
const {newText, date} = parseDate(text)
|
||||
result.text = newText
|
||||
result.date = date
|
||||
|
||||
return cleanupResult(result)
|
||||
}
|
||||
|
||||
const getItemsFromPrefix = (text, prefix) => {
|
||||
const items = []
|
||||
|
||||
const itemParts = text.split(prefix)
|
||||
itemParts.forEach((p, index) => {
|
||||
// First part contains the rest
|
||||
if (index < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
let labelText
|
||||
if (p.charAt(0) === `'`) {
|
||||
labelText = p.split(`'`)[1]
|
||||
} else if (p.charAt(0) === `"`) {
|
||||
labelText = p.split(`"`)[1]
|
||||
} else {
|
||||
// Only until the next space
|
||||
labelText = p.split(' ')[0]
|
||||
}
|
||||
items.push(labelText)
|
||||
})
|
||||
|
||||
return Array.from(new Set(items))
|
||||
}
|
||||
|
||||
const getPriority = text => {
|
||||
const ps = getItemsFromPrefix(text, PRIORITY_PREFIX)
|
||||
if (ps.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const p of ps) {
|
||||
for (const pi in priorities) {
|
||||
if (priorities[pi] === parseInt(p)) {
|
||||
return parseInt(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const cleanupItemText = (text, items, prefix) => {
|
||||
items.forEach(l => {
|
||||
text = text
|
||||
.replace(`${prefix}'${l}' `, '')
|
||||
.replace(`${prefix}'${l}'`, '')
|
||||
.replace(`${prefix}"${l}" `, '')
|
||||
.replace(`${prefix}"${l}"`, '')
|
||||
.replace(`${prefix}${l} `, '')
|
||||
.replace(`${prefix}${l}`, '')
|
||||
})
|
||||
return text
|
||||
}
|
||||
|
||||
const cleanupResult = result => {
|
||||
result.text = cleanupItemText(result.text, result.labels, LABEL_PREFIX)
|
||||
result.text = cleanupItemText(result.text, [result.list], LIST_PREFIX)
|
||||
result.text = cleanupItemText(result.text, [result.priority], PRIORITY_PREFIX)
|
||||
result.text = cleanupItemText(result.text, result.assignees, ASSIGNEE_PREFIX)
|
||||
result.text = result.text.trim()
|
||||
|
||||
return result
|
||||
}
|
|
@ -1,409 +0,0 @@
|
|||
import {parseTaskText} from './parseTaskText'
|
||||
import {getDateFromText, getDateFromTextIn} from './time/parseDate'
|
||||
import {calculateDayInterval} from './time/calculateDayInterval'
|
||||
import priorities from '../models/priorities.json'
|
||||
|
||||
describe('Parse Task Text', () => {
|
||||
it('should return text with no intents as is', () => {
|
||||
expect(parseTaskText('Lorem Ipsum').text).toBe('Lorem Ipsum')
|
||||
})
|
||||
|
||||
describe('Date Parsing', () => {
|
||||
it('should not return any date if none was provided', () => {
|
||||
const result = parseTaskText('Lorem Ipsum')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
it('should ignore casing', () => {
|
||||
const result = parseTaskText('Lorem Ipsum ToDay')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const now = new Date()
|
||||
expect(result.date.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(now.getMonth())
|
||||
expect(result.date.getDate()).toBe(now.getDate())
|
||||
})
|
||||
it('should recognize today', () => {
|
||||
const result = parseTaskText('Lorem Ipsum today')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const now = new Date()
|
||||
expect(result.date.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(now.getMonth())
|
||||
expect(result.date.getDate()).toBe(now.getDate())
|
||||
})
|
||||
describe('should recognize today with a time', () => {
|
||||
const cases = {
|
||||
'at 15:00': '15:0',
|
||||
'@ 15:00': '15:0',
|
||||
'at 15:30': '15:30',
|
||||
'@ 3pm': '15:0',
|
||||
'at 3pm': '15:0',
|
||||
'at 3 pm': '15:0',
|
||||
'at 3am': '3:0',
|
||||
'at 3:12 am': '3:12',
|
||||
'at 3:12 pm': '15:12',
|
||||
}
|
||||
|
||||
for (const c in cases) {
|
||||
it('should recognize today with a time ' + c, () => {
|
||||
const result = parseTaskText('Lorem Ipsum today ' + c)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const now = new Date()
|
||||
expect(result.date.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(now.getMonth())
|
||||
expect(result.date.getDate()).toBe(now.getDate())
|
||||
expect(`${result.date.getHours()}:${result.date.getMinutes()}`).toBe(cases[c])
|
||||
expect(result.date.getSeconds()).toBe(0)
|
||||
})
|
||||
}
|
||||
})
|
||||
it('should recognize tomorrow', () => {
|
||||
const result = parseTaskText('Lorem Ipsum tomorrow')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
expect(result.date.getFullYear()).toBe(tomorrow.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(tomorrow.getMonth())
|
||||
expect(result.date.getDate()).toBe(tomorrow.getDate())
|
||||
})
|
||||
it('should recognize next monday', () => {
|
||||
const result = parseTaskText('Lorem Ipsum next monday')
|
||||
|
||||
const untilNextMonday = calculateDayInterval('nextMonday')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextMonday = new Date()
|
||||
nextMonday.setDate(nextMonday.getDate() + untilNextMonday)
|
||||
expect(result.date.getFullYear()).toBe(nextMonday.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(nextMonday.getMonth())
|
||||
expect(result.date.getDate()).toBe(nextMonday.getDate())
|
||||
})
|
||||
it('should recognize this weekend', () => {
|
||||
const result = parseTaskText('Lorem Ipsum this weekend')
|
||||
|
||||
const untilThisWeekend = calculateDayInterval('thisWeekend')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const thisWeekend = new Date()
|
||||
thisWeekend.setDate(thisWeekend.getDate() + untilThisWeekend)
|
||||
expect(result.date.getFullYear()).toBe(thisWeekend.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(thisWeekend.getMonth())
|
||||
expect(result.date.getDate()).toBe(thisWeekend.getDate())
|
||||
})
|
||||
it('should recognize later this week', () => {
|
||||
const result = parseTaskText('Lorem Ipsum later this week')
|
||||
|
||||
const untilLaterThisWeek = calculateDayInterval('laterThisWeek')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const laterThisWeek = new Date()
|
||||
laterThisWeek.setDate(laterThisWeek.getDate() + untilLaterThisWeek)
|
||||
expect(result.date.getFullYear()).toBe(laterThisWeek.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(laterThisWeek.getMonth())
|
||||
expect(result.date.getDate()).toBe(laterThisWeek.getDate())
|
||||
})
|
||||
it('should recognize later next week', () => {
|
||||
const result = parseTaskText('Lorem Ipsum later next week')
|
||||
|
||||
const untilLaterNextWeek = calculateDayInterval('laterNextWeek')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const laterNextWeek = new Date()
|
||||
laterNextWeek.setDate(laterNextWeek.getDate() + untilLaterNextWeek)
|
||||
expect(result.date.getFullYear()).toBe(laterNextWeek.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(laterNextWeek.getMonth())
|
||||
expect(result.date.getDate()).toBe(laterNextWeek.getDate())
|
||||
})
|
||||
it('should recognize next week', () => {
|
||||
const result = parseTaskText('Lorem Ipsum next week')
|
||||
|
||||
const untilNextWeek = calculateDayInterval('nextWeek')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextWeek = new Date()
|
||||
nextWeek.setDate(nextWeek.getDate() + untilNextWeek)
|
||||
expect(result.date.getFullYear()).toBe(nextWeek.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(nextWeek.getMonth())
|
||||
expect(result.date.getDate()).toBe(nextWeek.getDate())
|
||||
})
|
||||
it('should recognize next month', () => {
|
||||
const result = parseTaskText('Lorem Ipsum next month')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextMonth = new Date()
|
||||
nextMonth.setDate(1)
|
||||
nextMonth.setMonth(nextMonth.getMonth() + 1)
|
||||
expect(result.date.getFullYear()).toBe(nextMonth.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(nextMonth.getMonth())
|
||||
expect(result.date.getDate()).toBe(nextMonth.getDate())
|
||||
})
|
||||
it('should recognize a date', () => {
|
||||
const result = parseTaskText('Lorem Ipsum 06/26/2021')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const date = new Date()
|
||||
date.setFullYear(2021, 5, 26)
|
||||
expect(result.date.getFullYear()).toBe(date.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(date.getMonth())
|
||||
expect(result.date.getDate()).toBe(date.getDate())
|
||||
})
|
||||
it('should recognize end of month', () => {
|
||||
const result = parseTaskText('Lorem Ipsum end of month')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const curDate = new Date()
|
||||
const date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0)
|
||||
expect(result.date.getFullYear()).toBe(date.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(date.getMonth())
|
||||
expect(result.date.getDate()).toBe(date.getDate())
|
||||
})
|
||||
it('should recognize weekdays', () => {
|
||||
const result = parseTaskText('Lorem Ipsum thu')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextThursday = new Date()
|
||||
nextThursday.setDate(nextThursday.getDate() + ((4 + 7 - nextThursday.getDay()) % 7))
|
||||
expect(`${result.date.getFullYear()}-${result.date.getMonth()}-${result.date.getDate()}`).toBe(`${nextThursday.getFullYear()}-${nextThursday.getMonth()}-${nextThursday.getDate()}`)
|
||||
expect(+new Date(result.date)).toBeGreaterThan(+new Date() - 10) // In on thursdays, this may be different by one second and thus fails the test
|
||||
})
|
||||
it('should recognize weekdays with time', () => {
|
||||
const result = parseTaskText('Lorem Ipsum thu at 14:00')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextThursday = new Date()
|
||||
nextThursday.setDate(nextThursday.getDate() + ((4 + 7 - nextThursday.getDay()) % 7))
|
||||
expect(`${result.date.getFullYear()}-${result.date.getMonth()}-${result.date.getDate()}`).toBe(`${nextThursday.getFullYear()}-${nextThursday.getMonth()}-${nextThursday.getDate()}`)
|
||||
expect(`${result.date.getHours()}:${result.date.getMinutes()}`).toBe('14:0')
|
||||
expect(+new Date(result.date)).toBeGreaterThan(+new Date() - 10) // In on thursdays, this may be different by one second and thus fails the test
|
||||
})
|
||||
it('should recognize dates of the month in the past but next month', () => {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - 1)
|
||||
const result = parseTaskText(`Lorem Ipsum ${date.getDate()}nd`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.date.getDate()).toBe(date.getDate())
|
||||
expect(result.date.getMonth()).toBe(date.getMonth() + 1)
|
||||
})
|
||||
it('should recognize dates of the month in the future', () => {
|
||||
const date = new Date()
|
||||
const result = parseTaskText(`Lorem Ipsum ${date.getDate() + 1}nd`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.date.getDate()).toBe(date.getDate() + 1)
|
||||
})
|
||||
|
||||
describe('Parse date from text', () => {
|
||||
const now = new Date()
|
||||
now.setFullYear(2021, 5, 24)
|
||||
|
||||
const cases = {
|
||||
'Lorem Ipsum 06/08/2021 ad': '2021-6-8',
|
||||
'Lorem Ipsum 6/7/21 ad': '2021-6-7',
|
||||
'27/07/2021,': null,
|
||||
'2021/07/06,': '2021-7-6',
|
||||
'2021-07-06': '2021-7-6',
|
||||
'27 jan': '2022-1-27',
|
||||
'27/1': '2022-1-27',
|
||||
'27/01': '2022-1-27',
|
||||
'16/12': '2021-12-16',
|
||||
'01/27': '2022-1-27',
|
||||
'1/27': '2022-1-27',
|
||||
'Jan 27': '2022-1-27',
|
||||
'jan 27': '2022-1-27',
|
||||
'feb 21': '2022-2-21',
|
||||
'mar 21': '2022-3-21',
|
||||
'apr 21': '2022-4-21',
|
||||
'may 21': '2022-5-21',
|
||||
'jun 21': '2022-6-21',
|
||||
'jul 21': '2021-7-21',
|
||||
'aug 21': '2021-8-21',
|
||||
'sep 21': '2021-9-21',
|
||||
'oct 21': '2021-10-21',
|
||||
'nov 21': '2021-11-21',
|
||||
'dec 21': '2021-12-21',
|
||||
}
|
||||
|
||||
for (const c in cases) {
|
||||
it(`should parse '${c}' as '${cases[c]}'`, () => {
|
||||
const {date} = getDateFromText(c, now)
|
||||
if (date === null && cases[c] === null) {
|
||||
expect(date).toBeNull()
|
||||
return
|
||||
}
|
||||
|
||||
expect(`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`).toBe(cases[c])
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('Parse date from text in', () => {
|
||||
const now = new Date()
|
||||
now.setFullYear(2021, 5, 24)
|
||||
now.setHours(12)
|
||||
now.setMinutes(0)
|
||||
now.setSeconds(0)
|
||||
|
||||
const cases = {
|
||||
'Lorem Ipsum in 1 hour': '2021-6-24 13:0',
|
||||
'in 2 hours': '2021-6-24 14:0',
|
||||
'in 1 day': '2021-6-25 12:0',
|
||||
'in 2 days': '2021-6-26 12:0',
|
||||
'in 1 week': '2021-7-1 12:0',
|
||||
'in 2 weeks': '2021-7-8 12:0',
|
||||
'in 4 weeks': '2021-7-22 12:0',
|
||||
'in 1 month': '2021-7-24 12:0',
|
||||
'in 3 months': '2021-9-24 12:0',
|
||||
}
|
||||
|
||||
for (const c in cases) {
|
||||
it(`should parse '${c}' as '${cases[c]}'`, () => {
|
||||
const {date} = getDateFromTextIn(c, now)
|
||||
if (date === null && cases[c] === null) {
|
||||
expect(date).toBeNull()
|
||||
return
|
||||
}
|
||||
|
||||
expect(`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`).toBe(cases[c])
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('Labels', () => {
|
||||
it('should parse labels', () => {
|
||||
const result = parseTaskText('Lorem Ipsum @label1 @label2')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.labels).toHaveLength(2)
|
||||
expect(result.labels[0]).toBe('label1')
|
||||
expect(result.labels[1]).toBe('label2')
|
||||
})
|
||||
it('should parse labels from the start', () => {
|
||||
const result = parseTaskText('@label1 Lorem Ipsum @label2')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.labels).toHaveLength(2)
|
||||
expect(result.labels[0]).toBe('label1')
|
||||
expect(result.labels[1]).toBe('label2')
|
||||
})
|
||||
it('should resolve duplicate labels', () => {
|
||||
const result = parseTaskText('Lorem Ipsum @label1 @label1 @label2')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.labels).toHaveLength(2)
|
||||
expect(result.labels[0]).toBe('label1')
|
||||
expect(result.labels[1]).toBe('label2')
|
||||
})
|
||||
it('should correctly parse labels with spaces in them', () => {
|
||||
const result = parseTaskText(`Lorem @'label with space' Ipsum`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.labels).toHaveLength(1)
|
||||
expect(result.labels[0]).toBe('label with space')
|
||||
})
|
||||
it('should correctly parse labels with spaces in them and "', () => {
|
||||
const result = parseTaskText('Lorem @"label with space" Ipsum')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.labels).toHaveLength(1)
|
||||
expect(result.labels[0]).toBe('label with space')
|
||||
})
|
||||
})
|
||||
|
||||
describe('List', () => {
|
||||
it('should parse a list', () => {
|
||||
const result = parseTaskText('Lorem Ipsum #list')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.list).toBe('list')
|
||||
})
|
||||
it('should parse a list with a space in it', () => {
|
||||
const result = parseTaskText(`Lorem Ipsum #'list with long name'`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.list).toBe('list with long name')
|
||||
})
|
||||
it('should parse a list with a space in it and "', () => {
|
||||
const result = parseTaskText(`Lorem Ipsum #"list with long name"`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.list).toBe('list with long name')
|
||||
})
|
||||
it('should parse only the first list', () => {
|
||||
const result = parseTaskText(`Lorem Ipsum #list1 #list2 #list3`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum #list2 #list3')
|
||||
expect(result.list).toBe('list1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Priority', () => {
|
||||
for (const p in priorities) {
|
||||
it(`should parse priority ${p}`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum !${priorities[p]}`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.priority).toBe(priorities[p])
|
||||
})
|
||||
}
|
||||
it(`should not parse an invalid priority`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum !9999`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum !9999')
|
||||
expect(result.priority).toBe(null)
|
||||
})
|
||||
it(`should not parse an invalid priority but use the first valid one it finds`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum !9999 !1`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum !9999')
|
||||
expect(result.priority).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Assignee', () => {
|
||||
it('should parse an assignee', () => {
|
||||
const result = parseTaskText('Lorem Ipsum +user')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.assignees).toHaveLength(1)
|
||||
expect(result.assignees[0]).toBe('user')
|
||||
})
|
||||
it('should parse multiple assignees', () => {
|
||||
const result = parseTaskText('Lorem Ipsum +user1 +user2 +user3')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.assignees).toHaveLength(3)
|
||||
expect(result.assignees[0]).toBe('user1')
|
||||
expect(result.assignees[1]).toBe('user2')
|
||||
expect(result.assignees[2]).toBe('user3')
|
||||
})
|
||||
it('should parse avoid duplicate assignees', () => {
|
||||
const result = parseTaskText('Lorem Ipsum +user1 +user1 +user2')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.assignees).toHaveLength(2)
|
||||
expect(result.assignees[0]).toBe('user1')
|
||||
expect(result.assignees[1]).toBe('user2')
|
||||
})
|
||||
it('should parse an assignee with a space in it', () => {
|
||||
const result = parseTaskText(`Lorem Ipsum +'user with long name'`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.assignees).toHaveLength(1)
|
||||
expect(result.assignees[0]).toBe('user with long name')
|
||||
})
|
||||
it('should parse an assignee with a space in it and "', () => {
|
||||
const result = parseTaskText(`Lorem Ipsum +"user with long name"`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.assignees).toHaveLength(1)
|
||||
expect(result.assignees[0]).toBe('user with long name')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,15 +0,0 @@
|
|||
/**
|
||||
* This function replaces all text, no matter the case.
|
||||
*
|
||||
* See https://stackoverflow.com/a/7313467/10924593
|
||||
*
|
||||
* @parma str
|
||||
* @param search
|
||||
* @param replace
|
||||
* @returns {*}
|
||||
*/
|
||||
export const replaceAll = (str, search, replace) => {
|
||||
const esc = search.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
const reg = new RegExp(esc, 'ig');
|
||||
return str.replace(reg, replace);
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
const key = 'collapsedBuckets'
|
||||
|
||||
const getAllState = () => {
|
||||
const saved = localStorage.getItem(key)
|
||||
if (saved === null) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return JSON.parse(saved)
|
||||
}
|
||||
|
||||
export const saveCollapsedBucketState = (listId, collapsedBuckets) => {
|
||||
const state = getAllState()
|
||||
state[listId] = collapsedBuckets
|
||||
for (const bucketId in state[listId]) {
|
||||
if (!state[listId][bucketId]) {
|
||||
delete state[listId][bucketId]
|
||||
}
|
||||
}
|
||||
localStorage.setItem(key, JSON.stringify(state))
|
||||
}
|
||||
|
||||
export const getCollapsedBucketState = listId => {
|
||||
const state = getAllState()
|
||||
if (typeof state[listId] !== 'undefined') {
|
||||
return state[listId]
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import {createDateFromString} from '@/helpers/time/createDateFromString'
|
||||
import {format, formatDistance} from 'date-fns'
|
||||
import {enGB, de} from 'date-fns/locale'
|
||||
import { enGB, de } from 'date-fns/locale'
|
||||
|
||||
const locales = {enGB, de}
|
||||
|
||||
|
@ -30,7 +30,7 @@ export const formatDateSince = (date, $t) => {
|
|||
date = createDateFromString(date)
|
||||
|
||||
const currentDate = new Date()
|
||||
const distance = formatDistance(date, currentDate, {locale: locales[$t('date.locale')]})
|
||||
const distance = formatDistance(date, currentDate)
|
||||
|
||||
if (date > currentDate) {
|
||||
return $t('date.in', {date: distance})
|
||||
|
|
|
@ -1,290 +0,0 @@
|
|||
import {calculateDayInterval} from './calculateDayInterval'
|
||||
import {calculateNearestHours} from './calculateNearestHours'
|
||||
import {replaceAll} from '../replaceAll'
|
||||
|
||||
export const parseDate = text => {
|
||||
const lowerText = text.toLowerCase()
|
||||
|
||||
if (lowerText.includes('today')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('today')), 'today')
|
||||
}
|
||||
if (lowerText.includes('tomorrow')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('tomorrow')), 'tomorrow')
|
||||
}
|
||||
if (lowerText.includes('next monday')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('nextMonday')), 'next monday')
|
||||
}
|
||||
if (lowerText.includes('this weekend')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('thisWeekend')), 'this weekend')
|
||||
}
|
||||
if (lowerText.includes('later this week')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('laterThisWeek')), 'later this week')
|
||||
}
|
||||
if (lowerText.includes('later next week')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('laterNextWeek')), 'later next week')
|
||||
}
|
||||
if (lowerText.includes('next week')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('nextWeek')), 'next week')
|
||||
}
|
||||
if (lowerText.includes('next month')) {
|
||||
const date = new Date()
|
||||
date.setDate(1)
|
||||
date.setMonth(date.getMonth() + 1)
|
||||
date.setHours(calculateNearestHours(date))
|
||||
date.setMinutes(0)
|
||||
date.setSeconds(0)
|
||||
|
||||
return addTimeToDate(text, date, 'next month')
|
||||
}
|
||||
if (lowerText.includes('end of month')) {
|
||||
const curDate = new Date()
|
||||
const date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0)
|
||||
date.setHours(calculateNearestHours(date))
|
||||
date.setMinutes(0)
|
||||
date.setSeconds(0)
|
||||
|
||||
return addTimeToDate(text, date, 'end of month')
|
||||
}
|
||||
|
||||
let parsed = getDateFromWeekday(text)
|
||||
if (parsed.date !== null) {
|
||||
return addTimeToDate(text, parsed.date, parsed.foundText)
|
||||
}
|
||||
|
||||
parsed = getDayFromText(text)
|
||||
if (parsed.date !== null) {
|
||||
return addTimeToDate(text, parsed.date, parsed.foundText)
|
||||
}
|
||||
|
||||
parsed = getDateFromTextIn(text)
|
||||
if (parsed.date !== null) {
|
||||
return {
|
||||
newText: replaceAll(text, parsed.foundText, ''),
|
||||
date: parsed.date,
|
||||
}
|
||||
}
|
||||
|
||||
parsed = getDateFromText(text)
|
||||
|
||||
return {
|
||||
newText: replaceAll(text, parsed.foundText, ''),
|
||||
date: parsed.date,
|
||||
}
|
||||
}
|
||||
|
||||
const addTimeToDate = (text, date, match) => {
|
||||
const matcher = new RegExp(`(${match} (at|@) )([0-9][0-9]?(:[0-9][0-9]?)?( ?(a|p)m)?)`, 'ig')
|
||||
const results = matcher.exec(text)
|
||||
|
||||
if (results !== null) {
|
||||
const time = results[3]
|
||||
const parts = time.split(':')
|
||||
let hours = parseInt(parts[0])
|
||||
let minutes = 0
|
||||
if (time.endsWith('pm')) {
|
||||
hours += 12
|
||||
}
|
||||
if (parts.length > 1) {
|
||||
minutes = parseInt(parts[1])
|
||||
}
|
||||
|
||||
date.setHours(hours)
|
||||
date.setMinutes(minutes)
|
||||
date.setSeconds(0)
|
||||
}
|
||||
|
||||
const replace = results !== null ? results[0] : match
|
||||
return {
|
||||
newText: replaceAll(text, replace, ''),
|
||||
date: date,
|
||||
}
|
||||
}
|
||||
|
||||
export const getDateFromText = (text, now = new Date()) => {
|
||||
const fullDateRegex = /([0-9][0-9]?\/[0-9][0-9]?\/[0-9][0-9]([0-9][0-9])?|[0-9][0-9][0-9][0-9]\/[0-9][0-9]?\/[0-9][0-9]?|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?)/ig
|
||||
|
||||
// 1. Try parsing the text as a "usual" date, like 2021-06-24 or 06/24/2021
|
||||
let results = fullDateRegex.exec(text)
|
||||
let result = results === null ? null : results[0]
|
||||
let foundText = result
|
||||
let containsYear = true
|
||||
if (result === null) {
|
||||
// 2. Try parsing the date as something like "jan 21" or "21 jan"
|
||||
const monthRegex = /((jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec) [0-9][0-9]?|[0-9][0-9]? (jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec))/ig
|
||||
results = monthRegex.exec(text)
|
||||
result = results === null ? null : `${results[0]} ${now.getFullYear()}`
|
||||
foundText = results === null ? '' : results[0]
|
||||
containsYear = false
|
||||
|
||||
if (result === null) {
|
||||
// 3. Try parsing the date as "27/01" or "01/27"
|
||||
const monthNumericRegex = /([0-9][0-9]?\/[0-9][0-9]?)/ig
|
||||
results = monthNumericRegex.exec(text)
|
||||
|
||||
// Put the year before or after the date, depending on what works
|
||||
result = results === null ? null : `${now.getFullYear()}/${results[0]}`
|
||||
foundText = results === null ? '' : results[0]
|
||||
if (isNaN(new Date(result))) {
|
||||
result = results === null ? null : `${results[0]}/${now.getFullYear()}`
|
||||
}
|
||||
if (isNaN(new Date(result)) && results[0] !== null) {
|
||||
const parts = results[0].split('/')
|
||||
result = `${parts[1]}/${parts[0]}/${now.getFullYear()}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result === null) {
|
||||
return {
|
||||
foundText,
|
||||
date: null,
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date(result)
|
||||
if (isNaN(date)) {
|
||||
return {
|
||||
foundText,
|
||||
date: null,
|
||||
}
|
||||
}
|
||||
|
||||
if (!containsYear && date < now) {
|
||||
date.setFullYear(date.getFullYear() + 1)
|
||||
}
|
||||
|
||||
return {
|
||||
foundText,
|
||||
date,
|
||||
}
|
||||
}
|
||||
|
||||
export const getDateFromTextIn = (text, now = new Date()) => {
|
||||
const regex = /(in [0-9]+ (hours?|days?|weeks?|months?))/ig
|
||||
const results = regex.exec(text)
|
||||
if (results === null) {
|
||||
return {
|
||||
foundText: '',
|
||||
date: null,
|
||||
}
|
||||
}
|
||||
|
||||
let foundText = results[0]
|
||||
const date = new Date(now)
|
||||
const parts = foundText.split(' ')
|
||||
switch (parts[2]) {
|
||||
case 'hours':
|
||||
case 'hour':
|
||||
date.setHours(date.getHours() + parseInt(parts[1]))
|
||||
break
|
||||
case 'days':
|
||||
case 'day':
|
||||
date.setDate(date.getDate() + parseInt(parts[1]))
|
||||
break
|
||||
case 'weeks':
|
||||
case 'week':
|
||||
date.setDate(date.getDate() + parseInt(parts[1]) * 7)
|
||||
break
|
||||
case 'months':
|
||||
case 'month':
|
||||
date.setMonth(date.getMonth() + parseInt(parts[1]))
|
||||
break
|
||||
}
|
||||
|
||||
return {
|
||||
foundText,
|
||||
date,
|
||||
}
|
||||
}
|
||||
|
||||
const getDateFromWeekday = text => {
|
||||
const matcher = /(mon|monday|tue|tuesday|wed|wednesday|thu|thursday|fri|friday|sat|saturday|sun|sunday)/ig
|
||||
const results = matcher.exec(text)
|
||||
if (results === null) {
|
||||
return {
|
||||
foundText: null,
|
||||
date: null,
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date()
|
||||
const currentDay = date.getDay()
|
||||
let day = 0
|
||||
|
||||
switch (results[0]) {
|
||||
case 'mon':
|
||||
case 'monday':
|
||||
day = 1
|
||||
break
|
||||
case 'tue':
|
||||
case 'tuesday':
|
||||
day = 2
|
||||
break
|
||||
case 'wed':
|
||||
case 'wednesday':
|
||||
day = 3
|
||||
break
|
||||
case 'thu':
|
||||
case 'thursday':
|
||||
day = 4
|
||||
break
|
||||
case 'fri':
|
||||
case 'friday':
|
||||
day = 5
|
||||
break
|
||||
case 'sat':
|
||||
case 'saturday':
|
||||
day = 6
|
||||
break
|
||||
case 'sun':
|
||||
case 'sunday':
|
||||
day = 0
|
||||
break
|
||||
default:
|
||||
return {
|
||||
foundText: null,
|
||||
date: null,
|
||||
}
|
||||
}
|
||||
|
||||
const distance = (day + 7 - currentDay) % 7
|
||||
date.setDate(date.getDate() + distance)
|
||||
|
||||
return {
|
||||
foundText: results[0],
|
||||
date: date,
|
||||
}
|
||||
}
|
||||
|
||||
const getDayFromText = text => {
|
||||
const matcher = /(([1-2][0-9])|(3[01])|(0?[1-9]))(st|nd|rd|th|\.)/ig
|
||||
const results = matcher.exec(text)
|
||||
if (results === null) {
|
||||
return {
|
||||
foundText: null,
|
||||
date: null,
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date()
|
||||
date.setDate(parseInt(results[0]))
|
||||
|
||||
if (date < new Date()) {
|
||||
date.setMonth(date.getMonth() + 1)
|
||||
}
|
||||
|
||||
return {
|
||||
foundText: results[0],
|
||||
date: date,
|
||||
}
|
||||
}
|
||||
|
||||
const getDateFromInterval = interval => {
|
||||
const newDate = new Date()
|
||||
newDate.setDate(newDate.getDate() + interval)
|
||||
newDate.setHours(calculateNearestHours(newDate))
|
||||
newDate.setMinutes(0)
|
||||
newDate.setSeconds(0)
|
||||
|
||||
return newDate
|
||||
}
|
|
@ -1,309 +1,542 @@
|
|||
{
|
||||
"404": {
|
||||
"title": "Nicht gefunden",
|
||||
"text": "Die angeforderte Seite existiert nicht."
|
||||
},
|
||||
"filters": {
|
||||
"create": {
|
||||
"action": "Neuen gespeicherten Filter erstellen",
|
||||
"description": "Ein gespeicherter Filter ist eine virtuelle Liste, die bei jedem Zugriff aus einem Satz von Filtern errechnet wird. Einmal erstellt, erscheint er in einem speziellen Namensraum.",
|
||||
"title": "Einen gespeicherten Filter erstellen"
|
||||
"404": {
|
||||
"title": "Nicht gefunden",
|
||||
"text": "Die angeforderte Seite existiert nicht."
|
||||
},
|
||||
"attributes": {
|
||||
"descriptionPlaceholder": "Die Beschreibung steht hier …",
|
||||
"description": "Beschreibung",
|
||||
"titlePlaceholder": "Der gespeicherte Filtertitel steht hier …",
|
||||
"title": "Titel"
|
||||
"filters": {
|
||||
"create": {
|
||||
"action": "Neuen gespeicherten Filter erstellen",
|
||||
"description": "Ein gespeicherter Filter ist eine virtuelle Liste, die bei jedem Zugriff aus einem Satz von Filtern errechnet wird. Einmal erstellt, erscheint er in einem speziellen Namensraum.",
|
||||
"title": "Einen gespeicherten Filter erstellen"
|
||||
},
|
||||
"attributes": {
|
||||
"descriptionPlaceholder": "Die Beschreibung steht hier …",
|
||||
"description": "Beschreibung",
|
||||
"titlePlaceholder": "Der gespeicherte Filtertitel steht hier …",
|
||||
"title": "Titel"
|
||||
},
|
||||
"delete": {
|
||||
"header": "Diesen gespeicherten Filter löschen",
|
||||
"success": "Der Filter wurde erfolgreich gelöscht."
|
||||
},
|
||||
"edit": {
|
||||
"success": "Der Filter wurde erfolgreich gespeichert.",
|
||||
"title": "Diesen gespeicherten Filter bearbeiten"
|
||||
},
|
||||
"title": "Filter"
|
||||
},
|
||||
"delete": {
|
||||
"header": "Diesen gespeicherten Filter löschen",
|
||||
"success": "Der Filter wurde erfolgreich gelöscht."
|
||||
"sharing": {
|
||||
"authenticating": "Authentifizierung …",
|
||||
"invalidPassword": "Das Passwort ist ungültig.",
|
||||
"error": "Es ist ein Fehler aufgetreten."
|
||||
},
|
||||
"edit": {
|
||||
"success": "Der Filter wurde erfolgreich gespeichert.",
|
||||
"title": "Diesen gespeicherten Filter bearbeiten"
|
||||
"label": {
|
||||
"attributes": {
|
||||
"color": "Farbe",
|
||||
"description": "Beschreibung",
|
||||
"title": "Titel"
|
||||
}
|
||||
},
|
||||
"title": "Filter"
|
||||
},
|
||||
"sharing": {
|
||||
"authenticating": "Authentifizierung …",
|
||||
"invalidPassword": "Das Passwort ist ungültig.",
|
||||
"error": "Es ist ein Fehler aufgetreten."
|
||||
},
|
||||
"label": {
|
||||
"attributes": {
|
||||
"color": "Farbe",
|
||||
"description": "Beschreibung",
|
||||
"title": "Titel"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"search": "Suchen",
|
||||
"copy": "In Zwischenablage kopieren",
|
||||
"disable": "Deaktivieren",
|
||||
"confirm": "Bestätigen",
|
||||
"delete": "Löschen",
|
||||
"save": "Speichern",
|
||||
"loading": "Wird geladen …",
|
||||
"previous": "Vorherige",
|
||||
"next": "Weiter"
|
||||
},
|
||||
"task": {
|
||||
"delete": "Diese Aufgabe löschen",
|
||||
"new": "Eine neue Aufgabe erstellen",
|
||||
"task": "Aufgabe",
|
||||
"show": {
|
||||
"titleCurrent": "Aktuelle Aufgaben",
|
||||
"noTasks": "Nichts zu tun – Einen schönen Tag noch!",
|
||||
"today": "Heute",
|
||||
"nextWeek": "Nächste Woche"
|
||||
"misc": {
|
||||
"search": "Suchen",
|
||||
"copy": "In Zwischenablage kopieren",
|
||||
"disable": "Deaktivieren",
|
||||
"confirm": "Bestätigen",
|
||||
"delete": "Löschen",
|
||||
"save": "Speichern",
|
||||
"loading": "Wird geladen …",
|
||||
"previous": "Vorherige",
|
||||
"next": "Weiter",
|
||||
"poweredBy": "Angetrieben von Vikunja"
|
||||
},
|
||||
"detail": {
|
||||
"created": "Erstellt {0} von {1}",
|
||||
"undone": "Als unerledigt markieren",
|
||||
"done": "Fertig!",
|
||||
"move": "Aufgabe in eine andere Liste verschieben",
|
||||
"delete": {
|
||||
"header": "Diese Aufgabe löschen"
|
||||
},
|
||||
"deleteSuccess": "Die Aufgabe wurde erfolgreich gelöscht.",
|
||||
"updateSuccess": "Die Aufgabe wurde erfolgreich gespeichert.",
|
||||
"doneAt": "Erledigt {0}",
|
||||
"updated": "Aktualisiert {0}",
|
||||
"actions": {
|
||||
"priority": "Priorität einstellen",
|
||||
"reminders": "Erinnerungen einstellen",
|
||||
"relatedTasks": "Aufgabenbeziehungen hinzufügen",
|
||||
"attachments": "Anhänge hinzufügen",
|
||||
"delete": "Aufgabe löschen",
|
||||
"color": "Taskfarbe einstellen",
|
||||
"moveList": "Aufgabe verschieben"
|
||||
}
|
||||
"task": {
|
||||
"delete": "Diese Aufgabe löschen",
|
||||
"new": "Eine neue Aufgabe erstellen",
|
||||
"task": "Aufgabe",
|
||||
"show": {
|
||||
"titleCurrent": "Aktuelle Aufgaben",
|
||||
"noTasks": "Nichts zu tun. Einen schönen Tag noch!",
|
||||
"today": "Heute",
|
||||
"nextWeek": "Nächste Woche"
|
||||
},
|
||||
"detail": {
|
||||
"created": "Erstellt {0} von {1}",
|
||||
"undone": "Als unerledigt markieren",
|
||||
"done": "Fertig!",
|
||||
"move": "Aufgabe in eine andere Liste verschieben",
|
||||
"delete": {
|
||||
"header": "Diese Aufgabe löschen"
|
||||
},
|
||||
"deleteSuccess": "Die Aufgabe wurde erfolgreich gelöscht.",
|
||||
"updateSuccess": "Die Aufgabe wurde erfolgreich gespeichert.",
|
||||
"doneAt": "Erledigt {0}",
|
||||
"updated": "Aktualisiert {0}",
|
||||
"actions": {
|
||||
"priority": "Priorität einstellen",
|
||||
"reminders": "Erinnerungen einstellen",
|
||||
"relatedTasks": "Aufgabenbeziehungen hinzufügen",
|
||||
"attachments": "Anhänge hinzufügen",
|
||||
"delete": "Aufgabe löschen",
|
||||
"color": "Taskfarbe einstellen",
|
||||
"moveList": "Aufgabe verschieben"
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
"color": "Farbe",
|
||||
"done": "Fertig",
|
||||
"createdBy": "Erstellt von",
|
||||
"created": "Erstellt",
|
||||
"endDate": "Enddatum",
|
||||
"dueDate": "Fälligkeitsdatum",
|
||||
"title": "Titel",
|
||||
"startDate": "Anfangsdatum",
|
||||
"relatedTasks": "Verwandte Aufgaben",
|
||||
"priority": "Priorität",
|
||||
"percentDone": "% erledigt",
|
||||
"repeat": "Wiederholen",
|
||||
"reminders": "Erinnerungen",
|
||||
"updated": "Aktualisiert"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribeSuccess": "Du bist jetzt bei dieser {entity} abonniert",
|
||||
"unsubscribe": "Deabonnieren",
|
||||
"subscribe": "Abonnieren"
|
||||
},
|
||||
"attachment": {
|
||||
"download": "Herunterladen",
|
||||
"createdBy": "erstellt {0} von {1}"
|
||||
},
|
||||
"comment": {
|
||||
"placeholder": "Füge deinen Kommentar hinzu …",
|
||||
"creating": "Kommentar wird erstellt …",
|
||||
"edited": "bearbeitet {date}",
|
||||
"loading": "Kommentare werden geladen …",
|
||||
"addedSuccess": "Der Kommentar wurde erfolgreich hinzugefügt.",
|
||||
"deleteText2": "Dies kann nicht rückgängig gemacht werden!",
|
||||
"deleteText1": "Bist du sicher, dass du diesen Kommentar löschen willst?",
|
||||
"delete": "Diesen Kommentar löschen",
|
||||
"comment": "Kommentar"
|
||||
},
|
||||
"description": {
|
||||
"empty": "Noch keine Beschreibung vorhanden.",
|
||||
"placeholder": "Klicke hier, um eine Beschreibung einzugeben …"
|
||||
},
|
||||
"deferDueDate": {
|
||||
"1week": "1 Woche",
|
||||
"3days": "3 Tage",
|
||||
"1day": "1 Tag"
|
||||
},
|
||||
"assignee": {
|
||||
"unassignSuccess": "Die Zuweisung wurde erfolgreich aufgehoben.",
|
||||
"assignSuccess": "Der Benutzer wurde erfolgreich zugewiesen.",
|
||||
"selectPlaceholder": "Diese Benutzer zuweisen",
|
||||
"placeholder": "Tippe, um eine Benutzer zuzuweisen …"
|
||||
},
|
||||
"priority": {
|
||||
"doNow": "JETZT TUN",
|
||||
"urgent": "Dringend",
|
||||
"low": "Niedrig",
|
||||
"unset": "Nicht eingestellt"
|
||||
},
|
||||
"repeat": {
|
||||
"each": "Jede/n",
|
||||
"years": "Jahre",
|
||||
"months": "Monate",
|
||||
"weeks": "Wochen",
|
||||
"days": "Tage",
|
||||
"hours": "Stunden",
|
||||
"specifyAmount": "Gib einen Anzahl an …",
|
||||
"fromCurrentDate": "Ab aktuellem Datum",
|
||||
"monthly": "Monatlich",
|
||||
"mode": "Wiederholungsmodus",
|
||||
"everyMonth": "Jeden Monat",
|
||||
"everyWeek": "Jede Woche",
|
||||
"everyDay": "Jeden Tag"
|
||||
},
|
||||
"relation": {
|
||||
"deleteText2": "Dies kann nicht rückgängig gemacht werden!"
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
"color": "Farbe",
|
||||
"done": "Fertig",
|
||||
"createdBy": "Erstellt von",
|
||||
"created": "Erstellt",
|
||||
"endDate": "Enddatum",
|
||||
"dueDate": "Fälligkeitsdatum",
|
||||
"title": "Titel",
|
||||
"startDate": "Anfangsdatum",
|
||||
"relatedTasks": "Verwandte Aufgaben",
|
||||
"priority": "Priorität",
|
||||
"percentDone": "% erledigt",
|
||||
"repeat": "Wiederholen",
|
||||
"reminders": "Erinnerungen",
|
||||
"updated": "Aktualisiert"
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
"edit": {
|
||||
"delete": {
|
||||
"header": "Team löschen",
|
||||
"success": "Das Team wurde erfolgreich gelöscht."
|
||||
},
|
||||
"madeAdmin": "Das Teammitglied wurde erfolgreich zum Admin gemacht.",
|
||||
"madeMember": "Das Teammitglied wurde erfolgreich zum Mitglied gemacht.",
|
||||
"userAddedSuccess": "Das Teammitglied wurde erfolgreich hinzugefügt.",
|
||||
"success": "Das Team wurde erfolgreich aktualisiert.",
|
||||
"addUser": "Zum Team hinzufügen",
|
||||
"members": "Teammitglieder",
|
||||
"title": "Team „{team}“ bearbeiten",
|
||||
"deleteUser": {
|
||||
"header": "Benutzer aus dem Team entfernen"
|
||||
}
|
||||
"team": {
|
||||
"edit": {
|
||||
"delete": {
|
||||
"header": "Team löschen",
|
||||
"success": "Das Team wurde erfolgreich gelöscht.",
|
||||
"text2": "Alle Teammitglieder verlieren den Zugriff auf Listen und Namensräume, die mit diesem Team geteilt sind. Dies KANN NICHT RÜCKGÄNGIG gemacht werden!",
|
||||
"text1": "Bist du sicher, dass du dieses Team und alle seine Mitglieder löschen willst?"
|
||||
},
|
||||
"madeAdmin": "Das Teammitglied wurde erfolgreich zum Admin gemacht.",
|
||||
"madeMember": "Das Teammitglied wurde erfolgreich zum Mitglied gemacht.",
|
||||
"userAddedSuccess": "Das Teammitglied wurde erfolgreich hinzugefügt.",
|
||||
"success": "Das Team wurde erfolgreich aktualisiert.",
|
||||
"addUser": "Zum Team hinzufügen",
|
||||
"members": "Teammitglieder",
|
||||
"title": "Team „{team}“ bearbeiten",
|
||||
"deleteUser": {
|
||||
"header": "Benutzer aus dem Team entfernen",
|
||||
"success": "Der Benutzer wurde erfolgreich aus dem Team gelöscht.",
|
||||
"text2": "Er oder sie verliert den Zugriff auf alle Listen und Namensräumen, auf die dieses Team Zugriff hat. Dies KANN NICHT RÜCKGÄNGIG gemacht werden!",
|
||||
"text1": "Bist du sicher, dass du diese Benutzer aus dem Team entfernen willst?"
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"success": "Das Team wurde erfolgreich erstellt.",
|
||||
"title": "Ein neues Team erstellen"
|
||||
},
|
||||
"title": "Teams",
|
||||
"attributes": {
|
||||
"description": "Beschreibung",
|
||||
"descriptionPlaceholder": "Die Beschreibung des Teams steht hier …",
|
||||
"member": "Mitglied",
|
||||
"admin": "Admin",
|
||||
"name": "Teamname",
|
||||
"namePlaceholder": "Der Name des Teams steht hier …",
|
||||
"nameRequired": "Bitte gib einen Namen an."
|
||||
},
|
||||
"noTeams": "Du bist derzeit nicht Teil eines Teams."
|
||||
},
|
||||
"create": {
|
||||
"success": "Das Team wurde erfolgreich erstellt.",
|
||||
"title": "Ein neues Team erstellen"
|
||||
},
|
||||
"title": "Teams",
|
||||
"attributes": {
|
||||
"description": "Beschreibung",
|
||||
"descriptionPlaceholder": "Die Beschreibung des Teams steht hier …",
|
||||
"member": "Mitglied",
|
||||
"admin": "Admin",
|
||||
"name": "Teamname",
|
||||
"namePlaceholder": "Der Name des Teams steht hier …"
|
||||
}
|
||||
},
|
||||
"namespace": {
|
||||
"create": {
|
||||
"explanation": "Ein Namensraum ist eine Sammlung von Listen, die man teilen und verwenden kann, um seine Listen zu organisieren. Tatsächlich gehört jede Liste zu einem Namensraum.",
|
||||
"success": "Der Namensraum wurde erfolgreich angelegt.",
|
||||
"tooltip": "Was ist ein Namensraum?",
|
||||
"title": "Einen neuen Namensraum erstellen"
|
||||
},
|
||||
"attributes": {
|
||||
"isArchived": "Dieser Namensraum wird archiviert",
|
||||
"archived": "Ist archiviert",
|
||||
"color": "Farbe",
|
||||
"descriptionPlaceholder": "Die Beschreibung des Namensraums steht hier …",
|
||||
"description": "Beschreibung",
|
||||
"titlePlaceholder": "Der Titel des Namensraums steht hier …",
|
||||
"title": "Namensraumtitel"
|
||||
},
|
||||
"share": {
|
||||
"title": "„{namespace}“ teilen"
|
||||
},
|
||||
"edit": {
|
||||
"success": "Der Namensraum wurde erfolgreich aktualisiert.",
|
||||
"title": "„{namespace}“ bearbeiten"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Der Namensraum wurde erfolgreich gelöscht.",
|
||||
"text2": "Dies umfasst alle Listen und Aufgaben und kann NICHT rückgängig gemacht werden!",
|
||||
"title": "„{namespace}“ löschen"
|
||||
},
|
||||
"archive": {
|
||||
"description": "Wenn ein Namensraum archiviert ist, kann man keine neuen Listen erstellen oder ihn bearbeiten.",
|
||||
"success": "Der Namensraum wurde erfolgreich archiviert.",
|
||||
"titleUnarchive": "Archivierung von „{namespace}“ aufheben",
|
||||
"titleArchive": "„{namespace}“ archivieren"
|
||||
},
|
||||
"noLists": "Dieser Namensraum enthält keine Listen.",
|
||||
"title": "Namensräume & Listen",
|
||||
"unarchive": "Archivierung aufheben",
|
||||
"archived": "Archiviert",
|
||||
"showArchived": "Archivierte anzeigen"
|
||||
},
|
||||
"list": {
|
||||
"kanban": {
|
||||
"bucketTitleSavedSuccess": "Der Eimertitel wurde erfolgreich gespeichert.",
|
||||
"deleteBucketSuccess": "Der Eimer wurde erfolgreich gelöscht.",
|
||||
"deleteBucketText2": "Dies löscht keine Aufgaben, sondern verschiebt sie in den Standard-Eimer.",
|
||||
"deleteHeaderBucket": "Den Eimer löschen",
|
||||
"addBucket": "Einen neuen Eimer erstellen",
|
||||
"addAnotherTask": "Weitere Aufgabe hinzufügen",
|
||||
"addTask": "Eine Aufgabe hinzufügen",
|
||||
"doneBucket": "Erledigte-Dinge-Eimer",
|
||||
"noLimit": "Nicht eingestellt"
|
||||
},
|
||||
"table": {
|
||||
"columns": "Spalten",
|
||||
"title": "Tabelle"
|
||||
},
|
||||
"gantt": {
|
||||
"to": "An",
|
||||
"from": "Von",
|
||||
"day": "Tag",
|
||||
"month": "Monat",
|
||||
"default": "Standard",
|
||||
"size": "Größe",
|
||||
"showTasksWithoutDates": "Aufgaben anzeigen, für die keine Termine festgelegt sind",
|
||||
"title": "Gantt"
|
||||
"namespace": {
|
||||
"create": {
|
||||
"explanation": "Ein Namensraum ist eine Sammlung von Listen, die man teilen und verwenden kann, um seine Listen zu organisieren. Tatsächlich gehört jede Liste zu einem Namensraum.",
|
||||
"success": "Der Namensraum wurde erfolgreich angelegt.",
|
||||
"tooltip": "Was ist ein Namensraum?",
|
||||
"title": "Einen neuen Namensraum erstellen"
|
||||
},
|
||||
"attributes": {
|
||||
"isArchived": "Dieser Namensraum wird archiviert",
|
||||
"archived": "Ist archiviert",
|
||||
"color": "Farbe",
|
||||
"descriptionPlaceholder": "Die Beschreibung des Namensraums steht hier …",
|
||||
"description": "Beschreibung",
|
||||
"titlePlaceholder": "Der Titel des Namensraums steht hier …",
|
||||
"title": "Namensraumtitel"
|
||||
},
|
||||
"share": {
|
||||
"title": "„{namespace}“ teilen"
|
||||
},
|
||||
"edit": {
|
||||
"success": "Der Namensraum wurde erfolgreich aktualisiert.",
|
||||
"title": "„{namespace}“ bearbeiten"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Der Namensraum wurde erfolgreich gelöscht.",
|
||||
"text2": "Dies umfasst alle Listen und Aufgaben und kann NICHT rückgängig gemacht werden!",
|
||||
"title": "„{namespace}“ löschen"
|
||||
},
|
||||
"archive": {
|
||||
"description": "Wenn ein Namensraum archiviert ist, kann man keine neuen Listen erstellen oder ihn bearbeiten.",
|
||||
"success": "Der Namensraum wurde erfolgreich archiviert.",
|
||||
"titleUnarchive": "Archivierung von „{namespace}“ aufheben",
|
||||
"titleArchive": "„{namespace}“ archivieren"
|
||||
},
|
||||
"noLists": "Dieser Namensraum enthält keine Listen.",
|
||||
"title": "Namensräume und Listen",
|
||||
"unarchive": "Archivierung aufheben",
|
||||
"archived": "Archiviert",
|
||||
"showArchived": "Archivierte anzeigen",
|
||||
"namespace": "Namensraum"
|
||||
},
|
||||
"list": {
|
||||
"empty": "Diese Liste ist derzeit leer.",
|
||||
"addPlaceholder": "Eine neue Aufgabe hinzufügen …",
|
||||
"add": "Hinzufügen",
|
||||
"title": "Liste"
|
||||
"kanban": {
|
||||
"bucketTitleSavedSuccess": "Der Eimertitel wurde erfolgreich gespeichert.",
|
||||
"deleteBucketSuccess": "Der Eimer wurde erfolgreich gelöscht.",
|
||||
"deleteBucketText2": "Dies löscht keine Aufgaben, sondern verschiebt sie in den Standard-Eimer.",
|
||||
"deleteHeaderBucket": "Den Eimer löschen",
|
||||
"addBucket": "Einen neuen Eimer erstellen",
|
||||
"addAnotherTask": "Weitere Aufgabe hinzufügen",
|
||||
"addTask": "Eine Aufgabe hinzufügen",
|
||||
"doneBucket": "Erledigte-Dinge-Eimer",
|
||||
"noLimit": "Nicht eingestellt",
|
||||
"title": "Kanban"
|
||||
},
|
||||
"table": {
|
||||
"columns": "Spalten",
|
||||
"title": "Tabelle"
|
||||
},
|
||||
"gantt": {
|
||||
"to": "An",
|
||||
"from": "Von",
|
||||
"day": "Tag",
|
||||
"month": "Monat",
|
||||
"default": "Standard",
|
||||
"size": "Größe",
|
||||
"showTasksWithoutDates": "Aufgaben anzeigen, für die keine Termine festgelegt sind",
|
||||
"title": "Gantt"
|
||||
},
|
||||
"list": {
|
||||
"empty": "Diese Liste ist derzeit leer.",
|
||||
"addPlaceholder": "Eine neue Aufgabe hinzufügen …",
|
||||
"add": "Hinzufügen",
|
||||
"title": "Liste",
|
||||
"addTitleRequired": "Bitte gib einen Titel an."
|
||||
},
|
||||
"share": {
|
||||
"title": "„{Liste}“ teilen",
|
||||
"header": "Diese Liste teilen",
|
||||
"userTeam": {
|
||||
"notShared": "Noch nicht mit {type} geteilt.",
|
||||
"you": "Du",
|
||||
"shared": "Geteilt mit diesen {type}",
|
||||
"typeTeam": "Team | Teams",
|
||||
"typeUser": "Benutzer | Benutzer"
|
||||
},
|
||||
"attributes": {
|
||||
"sharedBy": "Geteilt von",
|
||||
"name": "Name",
|
||||
"link": "Link"
|
||||
},
|
||||
"right": {
|
||||
"admin": "Admin",
|
||||
"read": "Nur lesen"
|
||||
},
|
||||
"links": {
|
||||
"noName": "Kein Name angegeben",
|
||||
"passwordExplanation": "Bei der Authentifizierung wird der Benutzer aufgefordert, dieses Passwort einzugeben.",
|
||||
"password": "Passwort (optional)",
|
||||
"namePlaceholder": "z.B. Max Muster",
|
||||
"name": "Name (optional)",
|
||||
"create": "Erstelle einen neuen geteilten Link",
|
||||
"explanation": "Erstellt einen Link zu einer Liste, für User, ohne Vikunja Account.",
|
||||
"what": "Was ist ein geteilter Link?",
|
||||
"title": "Geteilter Link"
|
||||
},
|
||||
"share": "Teilen"
|
||||
},
|
||||
"edit": {
|
||||
"success": "Die Liste wurde erfolgreich aktualisiert.",
|
||||
"color": "Farbe",
|
||||
"descriptionPlaceholder": "Die Listenbeschreibung geht hier …",
|
||||
"description": "Beschreibung",
|
||||
"identifierPlaceholder": "Der Listenbezeichner geht hier …",
|
||||
"identifier": "Listebezeichner",
|
||||
"identifierTooltip": "Der Listenbezeichner kann zur eindeutigen Identifizierung einer Aufgabe über Listen hinweg verwendet werden. Man kann ihn auf leer setzen, um ihn zu deaktivieren.",
|
||||
"titlePlaceholder": "Der Titel der Liste steht hier …",
|
||||
"title": "„{list}“ bearbeiten",
|
||||
"header": "Diese Liste bearbeiten"
|
||||
},
|
||||
"duplicate": {
|
||||
"success": "Die Liste wurde erfolgreich dupliziert.",
|
||||
"label": "Duplizieren",
|
||||
"title": "Diese Liste duplizieren",
|
||||
"text": "Wähle einen Namensraum aus, der die duplizierte Liste enthalten soll:"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Die Liste wurde erfolgreich gelöscht.",
|
||||
"text2": "Dies umfasst alle Aufgaben und kann NICHT rückgängig gemacht werden!",
|
||||
"header": "Diese Liste löschen",
|
||||
"title": "„{list}“ löschen",
|
||||
"text1": "Bist du sicher, dass du diese Liste und alle ihre Inhalte löschen willst?"
|
||||
},
|
||||
"background": {
|
||||
"removeSuccess": "Der Hintergrund ist erfolgreich entfernt worden!",
|
||||
"success": "Der Hintergrund ist erfolgreich eingestellt worden!",
|
||||
"loadMore": "Mehr Fotos laden",
|
||||
"poweredByUnsplash": "Angetrieben von Unsplash",
|
||||
"searchPlaceholder": "Nach einem Hintergrund suchen …",
|
||||
"remove": "Hintergrund entfernen",
|
||||
"title": "Listenhintergrund festlegen",
|
||||
"upload": "Wähle einen Hintergrund von deinem Computer"
|
||||
},
|
||||
"archive": {
|
||||
"success": "Die Liste wurde erfolgreich archiviert.",
|
||||
"unarchive": "Archivierung dieser Liste aufheben",
|
||||
"archive": "Diese Liste archivieren",
|
||||
"title": "„{Liste}“ archivieren",
|
||||
"archiveText": "Du kannst diese Liste nicht bearbeiten oder neue Aufgaben erstellen, bis du das Archiv aufhebst.",
|
||||
"unarchiveText": "Du kannst neue Aufgaben erstellen oder sie bearbeiten."
|
||||
},
|
||||
"create": {
|
||||
"createdSuccess": "Die Liste wurde erfolgreich erstellt.",
|
||||
"titlePlaceholder": "Der Titel der Liste steht hier …",
|
||||
"header": "Eine neue Liste erstellen",
|
||||
"addTitleRequired": "Bitte gebe einen Namen an."
|
||||
},
|
||||
"color": "Farbe",
|
||||
"searchSelect": "Klicke auf oder drücke die Eingabetaste, um diese Liste auszuwählen",
|
||||
"search": "Tippe, um nach einer Liste zu suchen …",
|
||||
"lists": "Listen",
|
||||
"title": "Listentitel",
|
||||
"archived": "Diese Liste wird archiviert. Es ist nicht möglich, neue Aufgaben zu erstellen oder sie zu bearbeiten."
|
||||
},
|
||||
"share": {
|
||||
"title": "„{Liste}“ teilen",
|
||||
"header": "Diese Liste teilen"
|
||||
"user": {
|
||||
"settings": {
|
||||
"caldav": {
|
||||
"title": "CalDAV",
|
||||
"more": "Mehr Informationen über CalDAV in Vikunja",
|
||||
"howTo": "Sie können Vikunja mit CalDAV-Clients verbinden, um alle Aufgaben von verschiedenen Clients anzuzeigen und zu verwalten. Geben Sie diese URL in Ihren Client ein:"
|
||||
},
|
||||
"totp": {
|
||||
"disableSuccess": "Die Zwei-Faktor-Authentifizierung wurde erfolgreich deaktiviert.",
|
||||
"passcode": "Code",
|
||||
"enroll": "Einschreiben",
|
||||
"title": "Zwei-Faktor-Authentifizierung",
|
||||
"disable": "Zwei-Faktor-Authentifizierung ausschalten",
|
||||
"enterPassword": "Bitte gib dein Passwort ein",
|
||||
"setupSuccess": "Du hast die Zwei-Faktor-Authentifizierung erfolgreich eingerichtet.",
|
||||
"scanQR": "Alternativ kannst du auch diesen QR-Code scannen:",
|
||||
"finishSetupPart2": "Danach gib unten einen Code aus deiner Anwendung ein.",
|
||||
"finishSetupPart1": "Um die Aktivierung zu vollenden, benutze diesen Token in deiner TOTP App (sowie OTP oder ähnlich):",
|
||||
"confirmSuccess": "TOTP wurde verifiziert und ist nun verwendbar.",
|
||||
"passcodePlaceholder": "Ein von deiner TOTP-App generierter code"
|
||||
},
|
||||
"general": {
|
||||
"weekStartMonday": "Montag",
|
||||
"weekStartSunday": "Sonntag",
|
||||
"weekStart": "Woche beginnt am",
|
||||
"playSoundWhenDone": "Einen Ton abspielen, wenn Aufgaben als erledigt markiert werden",
|
||||
"discoverableByEmail": "Andere Benutzer können mich finden, wenn sie nach meiner kompletten E-Mail-Adresse suchen.",
|
||||
"discoverableByName": "Andere Benutzer können mich finden, wenn sie nach meinem Namen suchen.",
|
||||
"overdueReminders": "Mir jeden Morgen Erinnerungen für überfällige, unerledigte Aufgaben per E-Mail senden",
|
||||
"emailReminders": "Mir Erinnerungen für Aufgaben per E-Mail senden",
|
||||
"savedSuccess": "Die Einstellungen wurden aktualisiert.",
|
||||
"newName": "Der neue Name",
|
||||
"name": "Name",
|
||||
"title": "Allgemeine Einstellungen",
|
||||
"language": "Sprache"
|
||||
},
|
||||
"updateEmailNew": "Neue E-Mail-Adresse",
|
||||
"passwordUpdateSuccess": "Passwort aktualisiert.",
|
||||
"passwordsDontMatch": "Das neue Passwort und seine Bestätigung stimmen nicht überein.",
|
||||
"currentPassword": "Aktuelles Passwort",
|
||||
"newPasswordConfirm": "Neue Passwortbestätigung",
|
||||
"newPassword": "Neues Passwort",
|
||||
"title": "Einstellungen",
|
||||
"currentPasswordPlaceholder": "Dein aktuelles Passwort",
|
||||
"newPasswordTitle": "Aktualisiere dein Passwort",
|
||||
"avatar": {
|
||||
"gravatar": "Gravatar",
|
||||
"title": "Avatar",
|
||||
"setSuccess": "Der Avatar wurde erfolgreich gesetzt!",
|
||||
"statusUpdateSuccess": "Avatar-Status wurde erfolgreich aktualisiert!",
|
||||
"uploadAvatar": "Avatar hochladen",
|
||||
"upload": "Hochladen",
|
||||
"initials": "Initialen"
|
||||
},
|
||||
"updateEmailSuccess": "Deine E-Mail-Adresse wurde erfolgreich aktualisiert. Wir haben dir einen Link zur Bestätigung geschickt.",
|
||||
"updateEmailTitle": "Aktualisiere deine E-Mail-Adresse"
|
||||
},
|
||||
"auth": {
|
||||
"openIdStateError": "Zustand stimmt nicht überein, weigert sich fortzufahren!",
|
||||
"authenticating": "Authentifizierung …",
|
||||
"loginWith": "Mit {provider} anmelden",
|
||||
"register": "Registrieren",
|
||||
"login": "Anmelden",
|
||||
"totpPlaceholder": "z.B. 123456",
|
||||
"totpTitle": "Zwei-Faktor-Authentifizierungscode",
|
||||
"passwordsDontMatch": "Passwörter stimmen nicht überein",
|
||||
"passwordPlaceholder": "z.B. •••••••••••",
|
||||
"password": "Passwort",
|
||||
"emailPlaceholder": "z.B. frederic@vikunja.io",
|
||||
"email": "E-Mail-Adresse",
|
||||
"usernamePlaceholder": "z.B. frederick",
|
||||
"usernameEmail": "Benutzername oder E-Mail-Adresse",
|
||||
"username": "Benutzername",
|
||||
"logout": "Abmelden",
|
||||
"confirmEmailSuccess": "Du kannst dich jetzt mit deiner E-Mail-Adresse anmelden.",
|
||||
"resetPasswordSuccess": "Prüfe deinen Posteingang! Du solltest eine E-Mail mit Anweisungen zum Zurücksetzen deines Passworts erhalten haben.",
|
||||
"resetPasswordAction": "Mir einen Link zum Zurücksetzen des Passworts senden",
|
||||
"resetPassword": "Dein Passwort zurücksetzen",
|
||||
"passwordRepeat": "Gib dein Passwort erneut ein"
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
"success": "Die Liste wurde erfolgreich aktualisiert.",
|
||||
"color": "Farbe",
|
||||
"descriptionPlaceholder": "Die Listenbeschreibung geht hier …",
|
||||
"description": "Beschreibung",
|
||||
"identifierPlaceholder": "Der Listenbezeichner geht hier …",
|
||||
"identifier": "Listebezeichner",
|
||||
"identifierTooltip": "Der Listenbezeichner kann zur eindeutigen Identifizierung einer Aufgabe über Listen hinweg verwendet werden. Man kann ihn auf leer setzen, um ihn zu deaktivieren.",
|
||||
"titlePlaceholder": "Der Titel der Liste steht hier …",
|
||||
"title": "„{list}“ bearbeiten",
|
||||
"header": "Diese Liste bearbeiten"
|
||||
"home": {
|
||||
"list": {
|
||||
"new": "Eine neue Liste erstellen",
|
||||
"import": "Deine Daten in Vikunja importieren",
|
||||
"importText": "Oder importiere deine Listen und Aufgaben aus anderen Diensten in Vikunja:",
|
||||
"newText": "Du kannst eine neue Liste für deine neuen Aufgaben erstellen:"
|
||||
},
|
||||
"welcome": "Hallo {username}"
|
||||
},
|
||||
"duplicate": {
|
||||
"success": "Die Liste wurde erfolgreich dupliziert.",
|
||||
"label": "Duplizieren",
|
||||
"title": "Diese Liste duplizieren"
|
||||
"menu": {
|
||||
"setBackground": "Hintergrund einstellen",
|
||||
"unarchive": "Archivierung aufheben",
|
||||
"delete": "Löschen",
|
||||
"duplicate": "Duplizieren",
|
||||
"archive": "Archivieren",
|
||||
"edit": "Bearbeiten",
|
||||
"share": "Teilen",
|
||||
"newList": "Neue Liste"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Die Liste wurde erfolgreich gelöscht.",
|
||||
"text2": "Dies umfasst alle Aufgaben und kann NICHT rückgängig gemacht werden!",
|
||||
"header": "Diese Liste löschen",
|
||||
"title": "„{list}“ löschen"
|
||||
"update": {
|
||||
"do": "Jetzt aktualisieren",
|
||||
"available": "Es ist ein Aktualisierung für Vikunja verfügbar!"
|
||||
},
|
||||
"background": {
|
||||
"removeSuccess": "Der Hintergrund ist erfolgreich entfernt worden!",
|
||||
"success": "Der Hintergrund ist erfolgreich eingestellt worden!",
|
||||
"loadMore": "Mehr Fotos laden",
|
||||
"poweredByUnsplash": "Angetrieben von Unsplash",
|
||||
"searchPlaceholder": "Nach einem Hintergrund suchen …",
|
||||
"remove": "Hintergrund entfernen",
|
||||
"title": "Listenhintergrund festlegen"
|
||||
"keyboardShortcuts": {
|
||||
"task": {
|
||||
"assign": "Diese Aufgabe zu einem Benutzer zuweisen",
|
||||
"done": "Eine Aufgabe als erledigt markieren",
|
||||
"title": "Aufgabenseite"
|
||||
},
|
||||
"quickSearch": "Such-/Schnellaktionsleiste öffnen",
|
||||
"toggleMenu": "Das Menü umschalten",
|
||||
"currentPageOnly": "Diese Tastenkürzel funktionieren nur auf der aktuellen Seite.",
|
||||
"allPages": "Diese Tastenkürzel funktionieren auf allen Seiten.",
|
||||
"title": "Tastenkürzel"
|
||||
},
|
||||
"archive": {
|
||||
"success": "Die Liste wurde erfolgreich archiviert.",
|
||||
"unarchive": "Archivierung dieser Liste aufheben",
|
||||
"archive": "Diese Liste archivieren",
|
||||
"title": "„{Liste}“ archivieren"
|
||||
"quickActions": {
|
||||
"placeholder": "Gib einen Befehl oder eine Suche ein …",
|
||||
"commands": "Befehle",
|
||||
"hint": "Du kannst # verwenden, um nur nach Aufgaben zu suchen, *, um nur nach Listen zu suchen und @, um nur nach Teams zu suchen.",
|
||||
"cmds": {
|
||||
"newTeam": "Neues Team",
|
||||
"newNamespace": "Neuer Namensraum",
|
||||
"newList": "Neue Liste",
|
||||
"newTask": "Neue Aufgabe"
|
||||
},
|
||||
"createList": "Eine Liste im aktuellen Namensraum erstellen ({title})",
|
||||
"createTask": "Eine Aufgabe in der aktuellen Liste erstellen ({title})",
|
||||
"newTeam": "Gib den Namen des neuen Teams ein …",
|
||||
"newNamespace": "Gib den Titel des neuen Namensraumes ein …",
|
||||
"newTask": "Gib den Titel der neuen Aufgabe ein …",
|
||||
"newList": "Gib den Titel der neuen Liste ein …",
|
||||
"teams": "Teams",
|
||||
"lists": "Listen",
|
||||
"tasks": "Aufgaben"
|
||||
},
|
||||
"create": {
|
||||
"createdSuccess": "Die Liste wurde erfolgreich erstellt.",
|
||||
"titlePlaceholder": "Der Titel der Liste steht hier …",
|
||||
"header": "Eine neue Liste erstellen"
|
||||
"notification": {
|
||||
"explainer": "Benachrichtigungen werden hier angezeigt, wenn Aktionen für Namensräume, Listen oder Aufgaben, die du abonniert hast, ausgeführt werden.",
|
||||
"none": "Du hast keine Benachrichtigungen. Einen schönen Tag noch!"
|
||||
},
|
||||
"color": "Farbe"
|
||||
},
|
||||
"user": {
|
||||
"settings": {
|
||||
"caldav": {
|
||||
"title": "Caldav"
|
||||
},
|
||||
"totp": {
|
||||
"disableSuccess": "Die Zwei-Faktor-Authentifizierung wurde erfolgreich deaktiviert.",
|
||||
"passcode": "Passcode",
|
||||
"enroll": "Einschreiben",
|
||||
"title": "Zwei-Faktor-Authentifizierung"
|
||||
},
|
||||
"general": {
|
||||
"weekStartMonday": "Montag",
|
||||
"weekStartSunday": "Sonntag",
|
||||
"weekStart": "Woche beginnt am",
|
||||
"playSoundWhenDone": "Einen Ton abspielen, wenn Aufgaben als erledigt markiert werden",
|
||||
"discoverableByEmail": "Andere Benutzer mich finden lassen, wenn sie nach meiner vollständigen E-Mail suchen",
|
||||
"discoverableByName": "Andere Benutzer mich finden lassen, wenn sie nach meinem Namen suchen",
|
||||
"overdueReminders": "Mir jeden Morgen Erinnerungen für überfällige unerledigte Aufgaben per E-Mail senden",
|
||||
"emailReminders": "Mir Erinnerungen für Aufgaben per E-Mail senden",
|
||||
"savedSuccess": "Die Einstellungen wurden erfolgreich aktualisiert.",
|
||||
"newName": "Der neue Name",
|
||||
"name": "Name",
|
||||
"title": "Allgemeine Einstellungen"
|
||||
},
|
||||
"updateEmailNew": "Neue E-Mail-Adresse",
|
||||
"passwordUpdateSuccess": "Das Passwort wurde erfolgreich aktualisiert.",
|
||||
"passwordsDontMatch": "Das neue Passwort und seine Bestätigung stimmen nicht überein.",
|
||||
"currentPassword": "Aktuelles Passwort",
|
||||
"newPasswordConfirm": "Neue Passwortbestätigung",
|
||||
"newPassword": "Neues Passwort",
|
||||
"title": "Einstellungen"
|
||||
"loadingError": {
|
||||
"contact": "wende dich an uns",
|
||||
"tryAgain": "versuche erneut",
|
||||
"failed": "Das Laden ist fehlgeschlagen, bitte {0}. Wenn der Fehler weiterhin besteht, bitte {1}."
|
||||
},
|
||||
"auth": {
|
||||
"openIdStateError": "Zustand stimmt nicht überein, weigert sich fortzufahren!",
|
||||
"authenticating": "Authentifizierung …",
|
||||
"loginWith": "Mit {provider} anmelden",
|
||||
"register": "Registrieren",
|
||||
"login": "Anmelden",
|
||||
"totpPlaceholder": "z.B. 123456",
|
||||
"totpTitle": "Zwei-Faktor-Authentifizierungscode",
|
||||
"passwordsDontMatch": "Passwörter stimmen nicht überein",
|
||||
"passwordPlaceholder": "z.B. •••••••••••",
|
||||
"password": "Passwort",
|
||||
"emailPlaceholder": "z.B. frederic@vikunja.io",
|
||||
"email": "E-Mail-Adresse",
|
||||
"usernamePlaceholder": "z.B. frederick",
|
||||
"usernameEmail": "Benutzername oder E-Mail-Adresse",
|
||||
"username": "Benutzername"
|
||||
"apiConfig": {
|
||||
"success": "Verwendung der Vikunja-Installation unter „{domain}“.",
|
||||
"error": "Konnte die Vikunja-Installation unter „{domain}“ nicht finden oder verwenden.",
|
||||
"signInOn": "Melde dich bei deinem Vikunja-Konto auf {0} an",
|
||||
"change": "ändern",
|
||||
"urlPlaceholder": "z.B. https://localhost:3456",
|
||||
"url": "Vikunja-URL"
|
||||
},
|
||||
"error": {
|
||||
"11001": "Der gespeicherte Filter existiert nicht.",
|
||||
"7003": "Du hast keinen Zugriff auf diese Liste.",
|
||||
"3005": "Der Listentitel darf nicht leer sein.",
|
||||
"3004": "Du musst Leseberechtigungen für diese Liste haben, um diese Aktion ausführen zu können.",
|
||||
"3001": "Die Liste existiert nicht.",
|
||||
"1014": "Alte Passwort ist leer.",
|
||||
"1013": "Neues Passwort ist leer.",
|
||||
"1012": "E-Mail-Adresse des Benutzers nicht bestätigt.",
|
||||
"1011": "Falscher Benutzername oder falsches Passwort.",
|
||||
"1010": "Ungültiges E-Mail-Bestätigungs-Token.",
|
||||
"1009": "Ungültiges Token zum Zurücksetzen des Passworts.",
|
||||
"1008": "Es wird kein Token zum Zurücksetzen des Passworts bereitgestellt.",
|
||||
"1006": "Die Benutzer-ID konnte nicht ermittelt werden.",
|
||||
"1005": "Der Benutzer existiert nicht.",
|
||||
"1004": "Kein Benutzername und Passwort angegeben.",
|
||||
"1002": "Es existiert bereits ein Benutzer mit dieser E-Mail-Adresse.",
|
||||
"1001": "Es existiert bereits ein Benutzer mit diesem Benutzernamen.",
|
||||
"0001": "Das darfst du nicht tun.",
|
||||
"success": "Erfolg",
|
||||
"error": "Fehler"
|
||||
},
|
||||
"date": {
|
||||
"altFormatShort": "j M Y",
|
||||
"altFormatLong": "j M Y H:i",
|
||||
"ago": "vor {date}",
|
||||
"locale": "de"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"list": {
|
||||
"new": "Eine neue Liste erstellen"
|
||||
},
|
||||
"welcome": "Hallo {username}"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1 +1,78 @@
|
|||
{}
|
||||
{
|
||||
"list": {
|
||||
"share": {
|
||||
"links": {
|
||||
"removeText": "¿Está seguro de que desea eliminar este enlace compartido? Ya no será posible acceder a esta lista con este enlace compartido. ¡Esto no se puede deshacer!"
|
||||
}
|
||||
},
|
||||
"kanban": {
|
||||
"doneBucketHintExtended": "Todas las tareas que se trasladen al depósito de finalizadas se marcarán como realizadas automáticamente. Todas las tareas marcadas como realizadas desde otro lugar también se moverán."
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"settings": {
|
||||
"newPasswordTitle": "Actualiza tu contraseña",
|
||||
"newPassword": "Nueva contraseña",
|
||||
"currentPasswordPlaceholder": "Tu contraseña actual",
|
||||
"currentPassword": "Contraseña actual",
|
||||
"passwordsDontMatch": "La contraseña nueva y su confirmación no emparejan.",
|
||||
"title": "Opciones",
|
||||
"updateEmailTitle": "Actualiza tu dirección de correo electrónico",
|
||||
"newPasswordConfirm": "Confirmar contraseña",
|
||||
"passwordUpdateSuccess": "La contraseña se actualizó correctamente.",
|
||||
"general": {
|
||||
"savedSuccess": "Configuración actualizada.",
|
||||
"newName": "El nombre nuevo",
|
||||
"name": "Nombre",
|
||||
"title": "Configuración General"
|
||||
},
|
||||
"updateEmailSuccess": "Dirección de correo electrónico actualizada. Haga clic en el enlace del correo electrónico que se te ha enviado para confirmarlo.",
|
||||
"updateEmailNew": "Nueva dirección de correo electrónico"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Ingresar",
|
||||
"email": "Correo electrónico",
|
||||
"passwordRepeat": "Reescribe tu contraseña",
|
||||
"usernamePlaceholder": "p/ej. Federico",
|
||||
"confirmEmailSuccess": "Has confirmado correctamente tu correo electrónico. Ya podés conectarte.",
|
||||
"register": "Registrarse",
|
||||
"resetPasswordAction": "Envíame un enlace para restablecer la contraseña",
|
||||
"emailPlaceholder": "p/ej. frederic@vikunja.io",
|
||||
"resetPassword": "Restablecer tu contraseña",
|
||||
"loginWith": "Inicie sesión con {provider}",
|
||||
"passwordsDontMatch": "Las contraseñas no coinciden",
|
||||
"openIdStateError": "¡El estado no coincide, negándome a continuar!",
|
||||
"totpTitle": "Código de autenticación de dos factores",
|
||||
"password": "Contraseña",
|
||||
"passwordPlaceholder": "p/ej. •••••••••••",
|
||||
"totpPlaceholder": "p/ej. 123456",
|
||||
"resetPasswordSuccess": "¡Revisa tu bandeja de entrada! Debe tener un correo electrónico con instrucciones para restablecer su contraseña.",
|
||||
"logout": "Cerrar sesión",
|
||||
"authenticating": "Autenticando…",
|
||||
"usernameEmail": "Nombre de usuario o dirección de correo electrónico",
|
||||
"username": "Nombre de usuario"
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"attributes": {
|
||||
"title": "Título",
|
||||
"titlePlaceholder": "El título del filtro guardado va acá…"
|
||||
},
|
||||
"create": {
|
||||
"description": "Un filtro guardado es una lista virtual que se calcula a partir de un conjunto de filtros cada vez que se accede a él. Una vez creado, aparecerá en un espacio de nombres especial."
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"welcome": "Hola, {username}",
|
||||
"list": {
|
||||
"new": "Crear una lista nueva",
|
||||
"import": "Importa tus datos a Vikunja",
|
||||
"newText": "Podés crear una nueva lista para las tareas nuevas:",
|
||||
"importText": "O importa tus listas y tareas de otros servicios a Vikunja:"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"text": "La página solicitada no existe.",
|
||||
"title": "No encontrado"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,795 @@
|
|||
{}
|
||||
{
|
||||
"home": {
|
||||
"welcome": "Salut {username}",
|
||||
"list": {
|
||||
"import": "Importer tes données dans Vikunja",
|
||||
"importText": "Ou importe tes listes et tâches d’autres services dans Vikunja :",
|
||||
"new": "Créer une nouvelle liste",
|
||||
"newText": "Tu peux créer une nouvelle liste pour tes nouvelles tâches :"
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"attributes": {
|
||||
"descriptionPlaceholder": "Description de l’étiquette",
|
||||
"description": "Description",
|
||||
"titlePlaceholder": "Entre un nom d’étiquette…",
|
||||
"color": "Couleur",
|
||||
"title": "Nom"
|
||||
},
|
||||
"create": {
|
||||
"success": "Étiquette créée.",
|
||||
"titleRequired": "Indique un nom.",
|
||||
"title": "Créer une nouvelle étiquette",
|
||||
"header": "Nouvelle étiquette"
|
||||
},
|
||||
"search": "Écris pour rechercher une étiquette…",
|
||||
"newCTA": "Tu n’as actuellement aucune étiquette.",
|
||||
"description": "Clique sur une étiquette pour la modifier. Tu peux modifier toutes les étiquettes que tu as créées, tu peux utiliser toutes les étiquettes qui sont associées à une tâche dont tu as accès à la liste.",
|
||||
"manage": "Gérer les étiquettes",
|
||||
"title": "Étiquettes",
|
||||
"edit": {
|
||||
"header": "Modifier l’étiquette",
|
||||
"success": "Étiquette mise à jour.",
|
||||
"forbidden": "Tu ne peux pas modifier cette étiquette car elle ne t’appartient pas."
|
||||
},
|
||||
"deleteSuccess": "Étiquette supprimée."
|
||||
},
|
||||
"quickActions": {
|
||||
"createList": "Créer une liste dans l’espace de noms actuel ({title})",
|
||||
"createTask": "Créer une tâche dans la liste actuelle ({title})",
|
||||
"newTeam": "Entre le nom de la nouvelle équipe…",
|
||||
"newNamespace": "Entre le nom de l’espace de noms…",
|
||||
"newTask": "Entre le nom de la tâche…",
|
||||
"newList": "Entre le nom de la liste…",
|
||||
"teams": "Équipes",
|
||||
"lists": "Listes",
|
||||
"tasks": "Tâches",
|
||||
"hint": "Tu peux utiliser # pour rechercher uniquement les tâches, * pour rechercher uniquement les listes et @ pour rechercher uniquement les équipes.",
|
||||
"placeholder": "Écris une commande ou une recherche…",
|
||||
"commands": "Commandes",
|
||||
"cmds": {
|
||||
"newTeam": "Nouvelle équipe",
|
||||
"newNamespace": "Nouvel espace de noms",
|
||||
"newList": "Nouvelle liste",
|
||||
"newTask": "Nouvelle tâche"
|
||||
}
|
||||
},
|
||||
"notification": {
|
||||
"explainer": "Les notifications apparaissent ici lorsque des actions (pour les espaces de noms, les listes ou les tâches) auxquelles tu es abonné·e se produisent.",
|
||||
"none": "Tu n’as pas de notifications. Passe une bonne journée !"
|
||||
},
|
||||
"loadingError": {
|
||||
"contact": "contacte-nous",
|
||||
"tryAgain": "réessaye",
|
||||
"failed": "Le chargement a échoué, {0}. Si l’erreur persiste, {1}."
|
||||
},
|
||||
"apiConfig": {
|
||||
"success": "Utilisation de l’installation Vikunja à « {domain} ».",
|
||||
"error": "Impossible de trouver ou d’utiliser l’installation Vikunja à « {domain} ».",
|
||||
"signInOn": "Se connecter à ton compte Vikunja sur {0}",
|
||||
"change": "changer",
|
||||
"urlPlaceholder": "Par exemple : https://localhost:3456",
|
||||
"url": "URL Vikunja"
|
||||
},
|
||||
"menu": {
|
||||
"newList": "Nouvelle liste",
|
||||
"share": "Partager",
|
||||
"setBackground": "Définir l’arrière-plan",
|
||||
"unarchive": "Désarchiver",
|
||||
"delete": "Supprimer",
|
||||
"duplicate": "Dupliquer",
|
||||
"archive": "Archiver",
|
||||
"edit": "Modifier"
|
||||
},
|
||||
"update": {
|
||||
"do": "Mettre à jour maintenant",
|
||||
"available": "Il y a une mise à jour pour Vikunja disponible !"
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"task": {
|
||||
"related": "Modifier les tâches connexes de cette tâche",
|
||||
"attachment": "Ajouter une pièce jointe à cette tâche",
|
||||
"dueDate": "Modifier la date d’échéance de cette tâche",
|
||||
"labels": "Ajouter des étiquettes à cette tâche",
|
||||
"assign": "Assigner cette tâche à un·e utilisateur·rice",
|
||||
"done": "Marquer une tâche comme terminée",
|
||||
"title": "Page de tâche"
|
||||
},
|
||||
"quickSearch": "Ouvrir la barre de recherche/action rapide",
|
||||
"toggleMenu": "Basculer le menu",
|
||||
"currentPageOnly": "Fonctionnent uniquement sur la page en cours.",
|
||||
"allPages": "Fonctionne sur toutes les pages.",
|
||||
"title": "Raccourcis clavier"
|
||||
},
|
||||
"team": {
|
||||
"attributes": {
|
||||
"member": "Membre",
|
||||
"admin": "Admin",
|
||||
"descriptionPlaceholder": "La description des équipes va ici…",
|
||||
"description": "Description",
|
||||
"nameRequired": "Indique un nom.",
|
||||
"namePlaceholder": "Entre le nom de l’équipe…",
|
||||
"name": "Nom de l’équipe"
|
||||
},
|
||||
"edit": {
|
||||
"deleteUser": {
|
||||
"success": "Utilisateur·rice retiré·e de l’équipe.",
|
||||
"text2": "L’utilisateur·rice perdra l’accès à toutes les listes et espaces de noms auxquels cette équipe a accès. Ceci ne peut pas être annulé !",
|
||||
"text1": "Retirer cet·te utilisateur·rice de l’équipe ?",
|
||||
"header": "Retirer un·e utilisateur·rice de l’équipe"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Équipe supprimée.",
|
||||
"text2": "Tous les membres de l’équipe perdront l’accès aux listes et aux espaces de noms partagés avec cette équipe. Ceci ne peut pas être annulé !",
|
||||
"text1": "Supprimer cette équipe et tous ses membres ?",
|
||||
"header": "Supprimer l’équipe"
|
||||
},
|
||||
"madeAdmin": "Membre de l’équipe nommé admin.",
|
||||
"madeMember": "Le membre de l’équipe est devenu membre.",
|
||||
"userAddedSuccess": "Membre de l’équipe ajouté.",
|
||||
"success": "Équipe mise à jour.",
|
||||
"makeAdmin": "Rendre admin",
|
||||
"makeMember": "Ajouter comme membre",
|
||||
"addUser": "Ajouter à l’équipe",
|
||||
"search": "Écris pour rechercher un·e utilisateur·rice…",
|
||||
"members": "Membres de l’équipe",
|
||||
"title": "Modifier l’équipe « {team} »"
|
||||
},
|
||||
"create": {
|
||||
"success": "Équipe créée.",
|
||||
"title": "Créer une nouvelle équipe"
|
||||
},
|
||||
"noTeams": "Tu ne fais actuellement partie d’aucune équipe.",
|
||||
"title": "Équipes"
|
||||
},
|
||||
"task": {
|
||||
"repeat": {
|
||||
"years": "Années",
|
||||
"months": "Mois",
|
||||
"weeks": "Semaines",
|
||||
"days": "Jours",
|
||||
"hours": "Heures",
|
||||
"specifyAmount": "Indique un nombre…",
|
||||
"each": "Tous/toutes les",
|
||||
"fromCurrentDate": "À partir de la date actuelle",
|
||||
"monthly": "Mensuel",
|
||||
"mode": "Mode de répétition",
|
||||
"everyMonth": "Chaque mois",
|
||||
"everyWeek": "Chaque semaine",
|
||||
"everyDay": "Chaque jour"
|
||||
},
|
||||
"relation": {
|
||||
"deleteText2": "Ceci ne peut pas être annulé !",
|
||||
"deleteText1": "Supprimer cette relation de tâche ?",
|
||||
"delete": "Supprimer la relation de tâche",
|
||||
"noneYet": "Pas encore de relations de tâches.",
|
||||
"differentList": "Cette tâche appartient à une autre liste.",
|
||||
"createPlaceholder": "Ajouter cette tâche comme nouvelle tâche connexe",
|
||||
"searchPlaceholder": "Écris la recherche d’une nouvelle tâche à ajouter comme connexe…",
|
||||
"new": "Nouvelle relation de tâche",
|
||||
"add": "Ajouter une nouvelle relation de tâche"
|
||||
},
|
||||
"priority": {
|
||||
"doNow": "LE FAIRE MAINTENANT",
|
||||
"urgent": "Urgente",
|
||||
"high": "Élevée",
|
||||
"medium": "Moyenne",
|
||||
"low": "Faible",
|
||||
"unset": "Non définie"
|
||||
},
|
||||
"label": {
|
||||
"removeSuccess": "Étiquette retirée.",
|
||||
"createSuccess": "Étiquette créée.",
|
||||
"addSuccess": "Étiquette ajoutée.",
|
||||
"createPlaceholder": "Ajouter ceci comme nouvelle étiquette",
|
||||
"placeholder": "Écris pour ajouter une nouvelle étiquette…"
|
||||
},
|
||||
"assignee": {
|
||||
"unassignSuccess": "Désaffectation réussie.",
|
||||
"assignSuccess": "Affectation réussie.",
|
||||
"selectPlaceholder": "Affecter cet·te utilisateur·rice",
|
||||
"placeholder": "Écris pour affecter un·e utilisateur·rice…"
|
||||
},
|
||||
"description": {
|
||||
"empty": "Aucune description n’est encore disponible.",
|
||||
"placeholder": "Clique ici pour entrer une description…"
|
||||
},
|
||||
"deferDueDate": {
|
||||
"1week": "1 semaine",
|
||||
"3days": "3 jours",
|
||||
"1day": "1 jour",
|
||||
"title": "Reporter la date d’échéance"
|
||||
},
|
||||
"comment": {
|
||||
"addedSuccess": "Commentaire ajouté.",
|
||||
"deleteText2": "Ceci ne peut être annulé !",
|
||||
"deleteText1": "Supprimer ce commentaire ?",
|
||||
"delete": "Supprimer ce commentaire",
|
||||
"comment": "Commentaire",
|
||||
"placeholder": "Ajoute ton commentaire…",
|
||||
"creating": "Création d’un commentaire…",
|
||||
"edited": "modifié {date}",
|
||||
"loading": "Chargement des commentaires…",
|
||||
"title": "Commentaires"
|
||||
},
|
||||
"attachment": {
|
||||
"deleteText2": "Ceci ne peut être annulé !",
|
||||
"deleteText1": "Supprimer la pièce jointe {filename} ?",
|
||||
"delete": "Supprimer la pièce jointe",
|
||||
"drop": "Dépose les fichiers ici pour les téléverser",
|
||||
"upload": "Téléverser la pièce jointe",
|
||||
"download": "Télécharger",
|
||||
"createdBy": "créé {0} par {1}",
|
||||
"title": "Pièces jointes"
|
||||
},
|
||||
"subscription": {
|
||||
"unsubscribeSuccess": "Tu es maintenant désabonné·e de cette {entity}",
|
||||
"subscribeSuccess": "Tu es maintenant abonné·e à cette {entity}",
|
||||
"unsubscribe": "Se désabonner",
|
||||
"subscribe": "S’abonner",
|
||||
"notSubscribed": "Tu n’es pas abonné·e à cette {entity} et ne recevras pas de notifications pour les changements.",
|
||||
"subscribed": "Tu es actuellement abonné·e à cette {entity} et recevras des notifications pour les changements.",
|
||||
"subscribedThroughParent": "Tu ne peux pas te désabonner ici car tu es abonné·e à cette {entity} par le biais de son {parent}."
|
||||
},
|
||||
"attributes": {
|
||||
"updated": "Mis à jour",
|
||||
"title": "Nom",
|
||||
"startDate": "Date de début",
|
||||
"repeat": "Répéter",
|
||||
"reminders": "Rappels",
|
||||
"relatedTasks": "Tâches connexes",
|
||||
"priority": "Priorité",
|
||||
"percentDone": "% terminé",
|
||||
"labels": "Étiquettes",
|
||||
"endDate": "Date de fin",
|
||||
"dueDate": "Date d’échéance",
|
||||
"done": "Terminé",
|
||||
"description": "Description",
|
||||
"createdBy": "Créé par",
|
||||
"created": "Créé",
|
||||
"color": "Couleur",
|
||||
"assignees": "Attributaires"
|
||||
},
|
||||
"detail": {
|
||||
"actions": {
|
||||
"delete": "Supprimer la tâche",
|
||||
"color": "Définir la couleur de la tâche",
|
||||
"moveList": "Déplacer la tâche",
|
||||
"relatedTasks": "Ajouter des relations de tâches",
|
||||
"attachments": "Ajouter des pièces jointes",
|
||||
"percentDone": "Définir le pourcentage d’achèvement",
|
||||
"repeatAfter": "Définir un intervalle de répétition",
|
||||
"reminders": "Définir des rappels",
|
||||
"endDate": "Fixer une date de fin",
|
||||
"startDate": "Définir une date de début",
|
||||
"dueDate": "Définir l’échéance",
|
||||
"priority": "Définir la priorité",
|
||||
"label": "Ajouter des étiquettes",
|
||||
"assign": "Assigner cette tâche à un·e utilisateur·rice"
|
||||
},
|
||||
"delete": {
|
||||
"text2": "Ceci supprimera également toutes les pièces jointes, les rappels et les relations associés à cette tâche et ne pourra pas être annulé !",
|
||||
"text1": "Retirer cette tâche ?",
|
||||
"header": "Supprimer cette tâche"
|
||||
},
|
||||
"due": "Échéance {at}",
|
||||
"belongsToList": "Cette tâche appartient à la liste « {list} »",
|
||||
"deleteSuccess": "Tâche supprimée.",
|
||||
"updateSuccess": "Tâche enregistrée.",
|
||||
"doneAt": "Terminé {0}",
|
||||
"updated": "Mis à jour {0}",
|
||||
"created": "Créé {0} par {1}",
|
||||
"undone": "Marquer comme inachevé",
|
||||
"done": "Terminé !",
|
||||
"move": "Déplacer une tâche vers une autre liste",
|
||||
"chooseEndDate": "Clique ici pour fixer une date de fin",
|
||||
"chooseStartDate": "Clique ici pour fixer une date de début",
|
||||
"chooseDueDate": "Clique ici pour définir une date d’échéance"
|
||||
},
|
||||
"show": {
|
||||
"noTasks": "Rien à faire. Passe une bonne journée !",
|
||||
"nextMonth": "Le mois prochain",
|
||||
"nextWeek": "La semaine prochaine",
|
||||
"today": "Aujourd’hui",
|
||||
"until": "au",
|
||||
"from": "Tâches du",
|
||||
"titleCurrent": "Tâches actuelles",
|
||||
"current": "Tâches actuelles",
|
||||
"noDates": "Afficher les tâches sans date",
|
||||
"titleDates": "Tâches du {from} au {to}"
|
||||
},
|
||||
"openDetail": "Ouvrir la vue détaillée de la tâche",
|
||||
"undoneSuccess": "Tâche marquée comme non terminée.",
|
||||
"doneSuccess": "Tâche marquée comme terminée.",
|
||||
"addReminder": "Ajouter un nouveau rappel…",
|
||||
"createSuccess": "Tâche créée.",
|
||||
"delete": "Supprimer cette tâche",
|
||||
"new": "Créer une nouvelle tâche",
|
||||
"task": "Tâche"
|
||||
},
|
||||
"error": {
|
||||
"5009": "Accès en lecture à l’espace de noms nécessaire pour effectuer cette action.",
|
||||
"5006": "Le nom de l’espace de noms ne peut pas être vide.",
|
||||
"5003": "Tu n’as pas accès à l’espace de noms indiqué.",
|
||||
"5001": "L’espace de noms n’existe pas.",
|
||||
"4019": "Valeur de filtre de tâche invalide.",
|
||||
"4018": "Concaténateur de filtre de tâche invalide.",
|
||||
"4017": "Comparateur de filtre de tâche invalide.",
|
||||
"4016": "Champ de tâche invalide.",
|
||||
"4015": "Le commentaire de la tâche n’existe pas.",
|
||||
"4014": "L’ordre de tri des tâches est invalide.",
|
||||
"4013": "Paramètre de triage des tâches invalide.",
|
||||
"4012": "La pièce jointe de la tâche est trop grande.",
|
||||
"4011": "La pièce jointe de la tâche n’existe pas.",
|
||||
"4010": "Impossible de relier une tâche avec elle-même.",
|
||||
"4009": "La relation de tâche n’existe pas.",
|
||||
"4008": "Tu ne peux pas créer une relation de tâche qui existe déjà.",
|
||||
"4007": "Tu ne peux pas créer une relation de tâche avec un type de relation invalide.",
|
||||
"4006": "Tu ne peux pas définir une tâche parente comme tâche elle-même.",
|
||||
"4005": "Tu n’as pas le droit de voir la tâche.",
|
||||
"4004": "Besoin d’au moins une tâche lors de la modification en bloc de tâches.",
|
||||
"4003": "Toutes les tâches de modification en bloc doivent appartenir à la même liste.",
|
||||
"4002": "La tâche de liste n’existe pas.",
|
||||
"4001": "Le texte de la tâche de liste ne peut pas être vide.",
|
||||
"3008": "La liste est archivée et ne peut donc être consultée qu’en lecture seule. Ceci est également vrai pour toutes les tâches associées à cette liste.",
|
||||
"3007": "Une liste avec cet identifiant existe déjà.",
|
||||
"0001": "Tu n’as pas le droit de faire cela.",
|
||||
"13002": "Mot de passe de partage de lien invalide.",
|
||||
"13001": "Le mot de passe requis n’a pas été fourni pour ce partage de lien.",
|
||||
"12002": "Tu es déjà abonné·e à l’entité elle-même ou à une entité parente.",
|
||||
"7002": "L’utilisateur·rice a déjà accès à cette liste.",
|
||||
"6007": "L’équipe n’a pas accès à la liste pour effectuer cette action.",
|
||||
"6006": "Impossible de supprimer le dernier membre de l’équipe.",
|
||||
"6005": "L’utilisateur·rice est déjà membre de cette équipe.",
|
||||
"6004": "L’équipe a déjà accès à cet espace de noms ou à cette liste.",
|
||||
"6002": "L’équipe n’existe pas.",
|
||||
"6001": "Le nom de l’équipe ne peut pas être vide.",
|
||||
"5012": "L’espace de noms est archivé et ne peut donc être consulté qu’en lecture seule.",
|
||||
"5011": "Cet·e utilisateur·rice a déjà accès à cet espace de noms.",
|
||||
"5010": "Cette équipe n’a pas accès à cet espace de noms.",
|
||||
"3006": "Le partage de liste n’existe pas.",
|
||||
"3005": "Tu dois entrer un nom de liste.",
|
||||
"3004": "Tu dois avoir des droits de lecture sur cette liste pour effectuer cette action.",
|
||||
"3001": "La liste n’existe pas.",
|
||||
"2002": "Certaines des données de la requête étaient invalides.",
|
||||
"2001": "L’identifiant ne peut pas être vide ou égal à 0.",
|
||||
"1018": "Le paramètre du type d’avatar de l’utilisateur·rice est invalide.",
|
||||
"1017": "Code à usage unique invalide.",
|
||||
"1016": "Cette personne utilise un MDP à usage unique.",
|
||||
"1015": "Cette personne utilise déjà un MDP à usage unique.",
|
||||
"1014": "L’ancien mot de passe est vide.",
|
||||
"1013": "Le nouveau mot de passe est vide.",
|
||||
"1011": "Nom d’utilisateur·rice ou mot de passe erroné.",
|
||||
"1009": "Le jeton de réinitialisation du mot de passe est invalide.",
|
||||
"1008": "Aucun jeton de réinitialisation du mot de passe n’est fourni.",
|
||||
"1006": "Impossible d’obtenir l’identifiant de l’utilisateur·rice.",
|
||||
"success": "Succès",
|
||||
"error": "Erreur",
|
||||
"1005": "L’utilisateur·rice n’existe pas.",
|
||||
"1004": "Aucun nom d’utilisateur·rice et mot de passe n’a été indiqué.",
|
||||
"1001": "Un·e utilisateur·rice avec ce nom d’utilisateur·rice existe déjà.",
|
||||
"12001": "Le type d’entité d’abonnement est invalide.",
|
||||
"11002": "Les filtres enregistrés ne sont pas disponibles pour les partages de liens.",
|
||||
"11001": "Le filtre enregistré n’existe pas.",
|
||||
"9001": "Le droit est invalide.",
|
||||
"8003": "Tu n’as pas accès à cette étiquette.",
|
||||
"8002": "L’étiquette n’existe pas.",
|
||||
"8001": "Cette étiquette existe déjà sur cette tâche.",
|
||||
"7003": "Tu n’as pas accès à cette liste.",
|
||||
"1012": "L’adresse courriel de l’utilisateur·rice n’est pas confirmée.",
|
||||
"1010": "Jeton de confirmation de courriel invalide.",
|
||||
"1002": "Un·e utilisateur·rice avec cette adresse courriel existe déjà.",
|
||||
"10005": "Il peut y avoir seulement un seau des terminés par liste.",
|
||||
"10004": "Tu ne peux pas ajouter la tâche à ce seau car il a déjà dépassé la limite de tâches qu’il peut contenir.",
|
||||
"10003": "Tu ne peux pas supprimer le dernier seau d’une liste.",
|
||||
"10002": "Le seau ne fait pas partie de cette liste.",
|
||||
"10001": "Le seau n’existe pas."
|
||||
},
|
||||
"filters": {
|
||||
"title": "Filtres",
|
||||
"edit": {
|
||||
"success": "Filtre enregistré.",
|
||||
"title": "Modifier ce filtre enregistré"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Filtre supprimé.",
|
||||
"text": "Supprimer ce filtre enregistré ?",
|
||||
"header": "Supprimer ce filtre enregistré"
|
||||
},
|
||||
"create": {
|
||||
"action": "Créer un nouveau filtre enregistré",
|
||||
"description": "Un filtre enregistré est une liste virtuelle qui est calculée à partir d’un ensemble de filtres à chaque fois qu’on y accède. Une fois créé, il apparaît dans un espace de noms spécial.",
|
||||
"title": "Créer un filtre enregistré"
|
||||
},
|
||||
"attributes": {
|
||||
"reminderRange": "Plage de dates de rappel",
|
||||
"endDateRange": "Plage de dates de fin",
|
||||
"startDateRange": "Plage de dates de début",
|
||||
"dueDateRange": "Plage de dates d’échéance",
|
||||
"enablePercentDone": "Par % d’achèvement",
|
||||
"enablePriority": "Activer le filtre par priorité",
|
||||
"showDoneTasks": "Afficher les tâches terminées",
|
||||
"requireAll": "Exiger tous les filtres pour qu’une tâche s’affiche",
|
||||
"includeNulls": "Inclure les tâches sans valeurs",
|
||||
"descriptionPlaceholder": "Écris une description…",
|
||||
"description": "Description",
|
||||
"titlePlaceholder": "Entre un nom de filtre enregistré…",
|
||||
"title": "Nom"
|
||||
}
|
||||
},
|
||||
"namespace": {
|
||||
"attributes": {
|
||||
"isArchived": "Cet espace de noms est archivé",
|
||||
"archived": "Est archivé",
|
||||
"color": "Couleur",
|
||||
"descriptionPlaceholder": "Entre la description de l’espace de noms…",
|
||||
"description": "Description",
|
||||
"titlePlaceholder": "Entre le nom de l’espace de noms…",
|
||||
"title": "Nom de l’espace de noms"
|
||||
},
|
||||
"share": {
|
||||
"title": "Partager « {namespace} »"
|
||||
},
|
||||
"edit": {
|
||||
"success": "Espace de noms mis à jour.",
|
||||
"title": "Modifier « {namespace} »"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Espace de noms supprimé.",
|
||||
"text2": "Ceci inclut toutes les listes et les tâches et ne peut être annulé !",
|
||||
"text1": "Supprimer cet espace de noms et tout son contenu ?",
|
||||
"title": "Supprimer « {namespace} »"
|
||||
},
|
||||
"archive": {
|
||||
"description": "L’archivage d’un espace de noms signifie qu’on ne peut pas créer de nouvelles listes dans cet espace, ni le modifier.",
|
||||
"success": "Espace de noms archivé.",
|
||||
"unarchiveText": "Tu pourras créer de nouvelles listes ou les modifier.",
|
||||
"archiveText": "Tu ne pourras pas modifier cet espace de noms ou créer de nouvelles listes tant que tu ne l’auras pas désarchivé. Ceci archivera également toutes les listes de cet espace de noms.",
|
||||
"titleUnarchive": "Désarchiver « {namespace} »",
|
||||
"titleArchive": "Archiver « {namespace} »"
|
||||
},
|
||||
"create": {
|
||||
"success": "Espace de noms créé.",
|
||||
"tooltip": "Qu’est-ce qu’un espace de noms ?",
|
||||
"explanation": "Des collections de listes pour partager et organiser vos listes. En fait, chaque liste appartient à un espace de noms.",
|
||||
"titleRequired": "Indique un nom.",
|
||||
"title": "Créer un nouvel espace de noms"
|
||||
},
|
||||
"search": "Écris pour rechercher un espace de noms…",
|
||||
"namespaces": "Espaces de noms",
|
||||
"createList": "Créer une nouvelle liste dans cet espace de noms.",
|
||||
"noLists": "Cet espace de noms ne contient pas de listes.",
|
||||
"archived": "Archivé",
|
||||
"unarchive": "Désarchiver",
|
||||
"noneAvailable": "Tu n’as pas d’espace de noms pour le moment.",
|
||||
"showArchived": "Montrer les archivés",
|
||||
"namespace": "Espace de noms",
|
||||
"title": "Espaces de noms et listes"
|
||||
},
|
||||
"list": {
|
||||
"kanban": {
|
||||
"addAnotherTask": "Ajouter une autre tâche",
|
||||
"addTask": "Ajouter une tâche",
|
||||
"addTaskPlaceholder": "Entre le nom de la tâche…",
|
||||
"noLimit": "Non défini",
|
||||
"limit": "Limite : {limit}",
|
||||
"title": "Kanban",
|
||||
"bucketLimitSavedSuccess": "Limite du seau enregistrée.",
|
||||
"bucketTitleSavedSuccess": "Nom du seau enregistré.",
|
||||
"deleteBucketSuccess": "Seau supprimé.",
|
||||
"deleteBucketText2": "Ceci ne supprimera pas les tâches mais les déplacera dans le seau par défaut.",
|
||||
"deleteBucketText1": "Supprimer ce seau ?",
|
||||
"addBucket": "Créer un nouveau seau",
|
||||
"deleteLast": "Tu ne peux pas supprimer le dernier seau.",
|
||||
"doneBucketSavedSuccess": "Seau des terminés enregistré.",
|
||||
"doneBucketHintExtended": "Toutes les tâches déplacées dans le seau des choses terminées seront marquées comme terminées automatiquement. Toutes les tâches marquées comme terminées ailleurs seront également déplacées.",
|
||||
"doneBucketHint": "Toutes les tâches déplacées dans ce seau seront automatiquement marquées comme faites.",
|
||||
"deleteHeaderBucket": "Supprimer le seau",
|
||||
"addBucketPlaceholder": "Entre le nouveau nom du seau…",
|
||||
"doneBucket": "Seau des terminés"
|
||||
},
|
||||
"table": {
|
||||
"columns": "Colonnes",
|
||||
"title": "Tableau"
|
||||
},
|
||||
"gantt": {
|
||||
"noDates": "Aucune date n’a été fixée pour cette tâche.",
|
||||
"to": "À",
|
||||
"from": "De",
|
||||
"day": "Jour",
|
||||
"month": "Mois",
|
||||
"default": "Par défaut",
|
||||
"size": "Taille",
|
||||
"showTasksWithoutDates": "Afficher les tâches pour lesquelles aucune date n’a été fixée",
|
||||
"title": "Gantt"
|
||||
},
|
||||
"list": {
|
||||
"editTask": "Modifier la tâche",
|
||||
"newTaskCta": "Créer une nouvelle tâche.",
|
||||
"empty": "Cette liste est actuellement vide.",
|
||||
"addTitleRequired": "Indique un nom.",
|
||||
"addPlaceholder": "Ajouter une nouvelle tâche…",
|
||||
"add": "Ajouter",
|
||||
"title": "Liste"
|
||||
},
|
||||
"share": {
|
||||
"attributes": {
|
||||
"delete": "Supprimer",
|
||||
"right": "Droit",
|
||||
"sharedBy": "Partagé par",
|
||||
"name": "Nom",
|
||||
"link": "Lien"
|
||||
},
|
||||
"right": {
|
||||
"admin": "Admin",
|
||||
"readWrite": "Lecture et écriture",
|
||||
"read": "Lecture seule",
|
||||
"title": "Droit"
|
||||
},
|
||||
"userTeam": {
|
||||
"updatedSuccess": "{type} ajouté.",
|
||||
"addedSuccess": "{type} ajouté.",
|
||||
"removeSuccess": "{sharable} retiré de {type}.",
|
||||
"removeText": "Retirer ce {sharable} du {type} ? Ceci ne peut pas être annulé !",
|
||||
"removeHeader": "Retirer un {type} de la liste {sharable}",
|
||||
"notShared": "Pas encore partagé avec des {type}.",
|
||||
"you": "Toi",
|
||||
"shared": "Partagé avec ces {type}",
|
||||
"typeTeam": "équipe | équipes",
|
||||
"typeUser": "utilisateur·rice | utilisateur·rice·s"
|
||||
},
|
||||
"links": {
|
||||
"deleteSuccess": "Lien supprimé",
|
||||
"createSuccess": "Partage créé.",
|
||||
"create": "Créer un nouveau lien de partage",
|
||||
"nameExplanation": "Toutes les actions effectuées par ce partage de lien apparaîtront avec le nom.",
|
||||
"removeText": "Retirer ce partage de lien ? Il ne sera plus possible d’accéder à cette liste avec ce partage de lien. Cette opération ne peut être annulée !",
|
||||
"remove": "Retirer un lien de partage",
|
||||
"noName": "Aucun nom défini",
|
||||
"passwordExplanation": "L’utilisateur·rice doit saisir ce mot de passe pour se connecter.",
|
||||
"password": "Mot de passe (facultatif)",
|
||||
"namePlaceholder": "p. ex. Lorem Ipsum",
|
||||
"name": "Nom (facultatif)",
|
||||
"explanation": "Permet de partager une liste avec les personnes qui n’ont pas de compte sur Vikunja.",
|
||||
"what": "Qu’est-ce qu’un lien de partage ?",
|
||||
"title": "Liens de partage"
|
||||
},
|
||||
"share": "Partager",
|
||||
"title": "Partager « {list} »",
|
||||
"header": "Partager cette liste"
|
||||
},
|
||||
"edit": {
|
||||
"success": "Liste mise à jour.",
|
||||
"color": "Couleur",
|
||||
"descriptionPlaceholder": "Entre la description de la liste…",
|
||||
"description": "Description",
|
||||
"identifierPlaceholder": "L’identifiant de la liste va ici…",
|
||||
"identifierTooltip": "L’identifiant de liste peut être utilisé pour identifier de manière unique une tâche dans toutes les listes. Tu peux le régler sur vide pour le désactiver.",
|
||||
"identifier": "Identifiant de la liste",
|
||||
"titlePlaceholder": "Entre le nom de la liste…",
|
||||
"title": "Modifier « {list} »",
|
||||
"header": "Modifier cette liste"
|
||||
},
|
||||
"duplicate": {
|
||||
"success": "Liste dupliquée.",
|
||||
"text": "Sélectionne un espace de noms qui doit contenir la liste dupliquée :",
|
||||
"label": "Dupliquer",
|
||||
"title": "Dupliquer cette liste"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Liste supprimée.",
|
||||
"text2": "Ceci inclut toutes les tâches et ne peut pas être annulé !",
|
||||
"text1": "Supprimer cette liste et tout son contenu ?",
|
||||
"header": "Supprimer cette liste",
|
||||
"title": "Supprimer « {list} »"
|
||||
},
|
||||
"background": {
|
||||
"removeSuccess": "Arrière-plan supprimé.",
|
||||
"success": "Arrière-plan défini.",
|
||||
"loadMore": "Charger plus de photos",
|
||||
"poweredByUnsplash": "Propulsé par Unsplash",
|
||||
"searchPlaceholder": "Rechercher un arrière-plan…",
|
||||
"upload": "Choisis un arrière-plan depuis ton ordinateur",
|
||||
"remove": "Retirer l’arrière-plan",
|
||||
"title": "Définir l’arrière-plan de la liste"
|
||||
},
|
||||
"archive": {
|
||||
"success": "Liste archivée.",
|
||||
"archiveText": "Tu ne pourras pas modifier cette liste ni créer de nouvelles tâches tant que tu ne l’auras pas désarchivée.",
|
||||
"unarchiveText": "Tu pourras créer de nouvelles tâches ou les modifier.",
|
||||
"unarchive": "Désarchiver cette liste",
|
||||
"archive": "Archiver cette liste",
|
||||
"title": "Archiver « {list} »"
|
||||
},
|
||||
"create": {
|
||||
"createdSuccess": "Liste créée.",
|
||||
"addTitleRequired": "Indique un nom.",
|
||||
"titlePlaceholder": "Entre le nom de la liste…",
|
||||
"header": "Créer une nouvelle liste"
|
||||
},
|
||||
"shared": "Listes partagées",
|
||||
"searchSelect": "Clique ou appuie sur la touche Entrée pour sélectionner cette liste",
|
||||
"search": "Écris pour rechercher une liste…",
|
||||
"lists": "Listes",
|
||||
"color": "Couleur",
|
||||
"title": "Nom de la liste",
|
||||
"archived": "Cette liste est archivée. Il n’est pas possible d’y créer de nouvelles tâches ou de les modifier."
|
||||
},
|
||||
"user": {
|
||||
"settings": {
|
||||
"totp": {
|
||||
"enroll": "S’inscrire",
|
||||
"disableSuccess": "Authentification à deux facteurs désactivée.",
|
||||
"confirmSuccess": "Configuration du MDP à usage unique confirmée et utilisable.",
|
||||
"disable": "Désactiver l’authentification à deux facteurs",
|
||||
"enterPassword": "Entre ton mot de passe",
|
||||
"setupSuccess": "Tu as maintenant configuré l’authentification à deux facteurs.",
|
||||
"passcodePlaceholder": "Un code généré par ton appli de mot de passe à usage unique",
|
||||
"passcode": "Code",
|
||||
"scanQR": "Tu peux également scanner ce code QR :",
|
||||
"finishSetupPart2": "Après cela, entre un code de ton application ci-dessous.",
|
||||
"finishSetupPart1": "Pour terminer ta configuration, utilise ce secret dans ton appli de mot de passe à usage unique (andOTP ou similaire) :",
|
||||
"title": "Authentification à deux facteurs"
|
||||
},
|
||||
"avatar": {
|
||||
"setSuccess": "Avatar défini.",
|
||||
"statusUpdateSuccess": "Statut de l’avatar mis à jour.",
|
||||
"uploadAvatar": "Téléverser l’avatar",
|
||||
"upload": "Téléverser",
|
||||
"gravatar": "Gravatar",
|
||||
"initials": "Initiales",
|
||||
"title": "Avatar"
|
||||
},
|
||||
"caldav": {
|
||||
"more": "Plus d’informations sur CalDAV dans Vikunja",
|
||||
"howTo": "Tu peux connecter Vikunja à des clients CalDAV pour visualiser et gérer toutes les tâches de différents clients. Entre cette URL dans ton client :",
|
||||
"title": "CalDAV"
|
||||
},
|
||||
"general": {
|
||||
"language": "Langue",
|
||||
"weekStartMonday": "lundi",
|
||||
"weekStartSunday": "dimanche",
|
||||
"weekStart": "La semaine commence le",
|
||||
"playSoundWhenDone": "Jouer un son lors du marquage des tâches comme étant effectuées",
|
||||
"discoverableByEmail": "Permettre aux autres de me trouver quand ils recherchent mon adresse courriel complète",
|
||||
"discoverableByName": "Permettre aux autres de me trouver quand ils recherchent mon nom",
|
||||
"overdueReminders": "M’envoyer des rappels pour les tâches en retard et non terminées par courriel chaque matin",
|
||||
"emailReminders": "M’envoyer des rappels de tâches par courriel",
|
||||
"savedSuccess": "Paramètres mis à jour.",
|
||||
"newName": "Le nouveau nom",
|
||||
"name": "Nom",
|
||||
"title": "Paramètres généraux"
|
||||
},
|
||||
"updateEmailSuccess": "Mise à jour de l’adresse électronique. Clique sur le lien dans le courriel qui t’a été envoyé pour le confirmer.",
|
||||
"updateEmailNew": "Nouvelle adresse courriel",
|
||||
"updateEmailTitle": "Mets à jour ton adresse électronique",
|
||||
"passwordUpdateSuccess": "Mot de passe mis à jour.",
|
||||
"passwordsDontMatch": "Le nouveau mot de passe et sa confirmation ne correspondent pas.",
|
||||
"currentPasswordPlaceholder": "Ton mot de passe actuel",
|
||||
"currentPassword": "Mot de passe actuel",
|
||||
"newPasswordConfirm": "Confirmation du nouveau mot de passe",
|
||||
"newPassword": "Nouveau mot de passe",
|
||||
"newPasswordTitle": "Mets à jour ton mot de passe",
|
||||
"title": "Paramètres"
|
||||
},
|
||||
"auth": {
|
||||
"logout": "Se déconnecter",
|
||||
"openIdStateError": "L’état ne correspond pas, impossible de continuer !",
|
||||
"authenticating": "Authentification…",
|
||||
"loginWith": "Se connecter avec {provider}",
|
||||
"register": "S’inscrire",
|
||||
"login": "Se connecter",
|
||||
"totpPlaceholder": "p. ex. 123456",
|
||||
"totpTitle": "Code d’authentification à deux facteurs",
|
||||
"confirmEmailSuccess": "Tu peux maintenant te connecter en utilisant votre adresse courriel.",
|
||||
"passwordsDontMatch": "Les mots de passe ne correspondent pas",
|
||||
"resetPasswordSuccess": "Vérifie ta boîte de réception ! Tu devrais recevoir un courriel contenant des instructions sur la manière de réinitialiser ton mot de passe.",
|
||||
"resetPasswordAction": "M’envoyer un lien de réinitialisation du mot de passe",
|
||||
"resetPassword": "Réinitialiser ton mot de passe",
|
||||
"passwordPlaceholder": "p. ex. •••••••••••",
|
||||
"passwordRepeat": "Retape ton mot de passe",
|
||||
"password": "Mot de passe",
|
||||
"usernamePlaceholder": "p. ex. frederick",
|
||||
"emailPlaceholder": "p. ex. frederic@vikunja.io",
|
||||
"email": "Adresse courriel",
|
||||
"usernameEmail": "Nom d’utilisateur·rice ou adresse courriel",
|
||||
"username": "Nom d’utilisateur·rice"
|
||||
}
|
||||
},
|
||||
"input": {
|
||||
"multiselect": {
|
||||
"selectPlaceholder": "Clique ou appuie sur la touche Entrée pour sélectionner",
|
||||
"createPlaceholder": "Créer un nouveau"
|
||||
},
|
||||
"editor": {
|
||||
"guide": "Guide",
|
||||
"sideBySide": "Côte à côte",
|
||||
"horizontalRule": "Règle horizontale",
|
||||
"table": "Tableau",
|
||||
"image": "Image",
|
||||
"link": "Lien",
|
||||
"cleanBlock": "Bloc propre",
|
||||
"orderedList": "Liste ordonnée",
|
||||
"unorderedList": "Liste non ordonnée",
|
||||
"quote": "Citation",
|
||||
"code": "Code",
|
||||
"strikethrough": "Barré",
|
||||
"italic": "Italique",
|
||||
"bold": "Gras",
|
||||
"headingBigger": "En-tête plus grand",
|
||||
"headingSmaller": "En-tête plus petit",
|
||||
"heading3": "En-tête 3",
|
||||
"heading2": "En-tête 2",
|
||||
"heading1": "En-tête 1",
|
||||
"done": "Terminé"
|
||||
},
|
||||
"datepicker": {
|
||||
"chooseDate": "Choisir une date",
|
||||
"nextWeek": "La semaine prochaine",
|
||||
"laterThisWeek": "Plus tard cette semaine",
|
||||
"thisWeekend": "Ce weekend",
|
||||
"nextMonday": "Lundi prochain",
|
||||
"tomorrow": "Demain",
|
||||
"today": "Aujourd’hui"
|
||||
},
|
||||
"resetColor": "Réinitialiser la couleur"
|
||||
},
|
||||
"misc": {
|
||||
"default": "Par défaut",
|
||||
"saved": "Enregistré !",
|
||||
"saving": "Enregistrement en cours…",
|
||||
"doit": "Faites-le !",
|
||||
"create": "Créer",
|
||||
"info": "Infos",
|
||||
"poweredBy": "Propulsé par Vikunja",
|
||||
"next": "Suivant",
|
||||
"previous": "Précédent",
|
||||
"searchPlaceholder": "Écris pour rechercher…",
|
||||
"search": "Rechercher",
|
||||
"copy": "Copier dans le presse-papier",
|
||||
"disable": "Désactiver",
|
||||
"refresh": "Actualiser",
|
||||
"cancel": "Annuler",
|
||||
"confirm": "Confirmer",
|
||||
"delete": "Supprimer",
|
||||
"save": "Enregistrer",
|
||||
"loading": "Chargement…"
|
||||
},
|
||||
"navigation": {
|
||||
"privacy": "Politique de confidentialité",
|
||||
"imprint": "Informations légales",
|
||||
"settings": "Paramètres",
|
||||
"upcoming": "À venir",
|
||||
"overview": "Vue d’ensemble"
|
||||
},
|
||||
"sharing": {
|
||||
"invalidPassword": "Le mot de passe est invalide.",
|
||||
"error": "Une erreur s’est produite.",
|
||||
"passwordRequired": "Cette liste partagée nécessite un mot de passe. Entre-le ci-dessous :",
|
||||
"authenticating": "Authentification…"
|
||||
},
|
||||
"404": {
|
||||
"text": "La page que tu as demandée n’existe pas.",
|
||||
"title": "Non trouvé"
|
||||
},
|
||||
"date": {
|
||||
"altFormatShort": "j M Y",
|
||||
"altFormatLong": "j M Y H:i",
|
||||
"ago": "il y a {date}",
|
||||
"in": "en {date}",
|
||||
"locale": "fr"
|
||||
},
|
||||
"migrate": {
|
||||
"confirm": "Je suis sûr·e, commencer à migrer maintenant !",
|
||||
"alreadyMigrated2": "Importer à nouveau est possible mais peut créer des doublons. Es-tu sûr·e ?",
|
||||
"alreadyMigrated1": "Il semble que tu aies déjà importé tes affaires de {name} le {date}.",
|
||||
"inProgress": "Importation en cours…",
|
||||
"getStarted": "Commencer",
|
||||
"authorize": "Autorise Vikunja à accéder à ton compte {name} en cliquant sur le bouton ci-dessous.",
|
||||
"descriptionDo": "Vikunja importera toutes les listes, tâches, notes, rappels et fichiers auxquels tu as accès.",
|
||||
"description": "Clique sur le logo d’un des services tiers ci-dessous pour commencer.",
|
||||
"import": "Importer tes données dans Vikunja",
|
||||
"titleService": "Importe tes données depuis {name} dans Vikunja",
|
||||
"title": "Migrer d’autres services vers Vikunja"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,785 @@
|
|||
{
|
||||
"namespace": {
|
||||
"namespace": "Navnerom",
|
||||
"edit": {
|
||||
"title": "Rediger «{namespace}»",
|
||||
"success": "Navnerom oppdatert."
|
||||
},
|
||||
"delete": {
|
||||
"title": "Slett «{namespace}»",
|
||||
"success": "Navnerom slettet.",
|
||||
"text2": "Dette inkluderer alle lister og gjøremål, og kan ikke angres!",
|
||||
"text1": "Slett dette navnerommet og hele dets innhold?"
|
||||
},
|
||||
"archive": {
|
||||
"success": "Navnerom arkivert.",
|
||||
"titleArchive": "Arkiver «{namespace}»",
|
||||
"titleUnarchive": "Avarkiver «{namespace}»",
|
||||
"description": "Hvis et navnerom arkiveres, kan du ikke opprette nye lister i det, eller redigere det.",
|
||||
"unarchiveText": "Du vil ikke kunne opprette nye lister eller redigere den.",
|
||||
"archiveText": "Du må av-arkivere dette navnerommet for å kunne opprette nye lister i det. Det vil også arkivere alle lister i det navnerommet."
|
||||
},
|
||||
"share": {
|
||||
"title": "Del «{namespace}»"
|
||||
},
|
||||
"create": {
|
||||
"title": "Opprett et nytt navnerom",
|
||||
"success": "Navnerom opprettet.",
|
||||
"tooltip": "Hva er et navnerom?",
|
||||
"explanation": "Samlinger av lister for å dele og organisere listene dine. Hver liste tilhører faktisk et navnerom.",
|
||||
"titleRequired": "Velg et navn."
|
||||
},
|
||||
"search": "Skriv for å søke etter et navnerom …",
|
||||
"namespaces": "Navnerommet",
|
||||
"createList": "Opprett en ny liste i dette navnerommet.",
|
||||
"noLists": "Dette navnerommet inneholder ingen lister.",
|
||||
"attributes": {
|
||||
"color": "Farge",
|
||||
"description": "Beskrivelse",
|
||||
"titlePlaceholder": "Skriv inn et navneromsnavn…",
|
||||
"title": "Navn på navnerom",
|
||||
"isArchived": "Dette navnerommet er arkivert",
|
||||
"archived": "Er arkivert"
|
||||
},
|
||||
"showArchived": "Vis arkiverte",
|
||||
"archived": "Arkivert",
|
||||
"title": "Navnerom og lister",
|
||||
"unarchive": "Av-arkiver",
|
||||
"noneAvailable": "Du har ingen navnerom akkurat nå."
|
||||
},
|
||||
"list": {
|
||||
"duplicate": {
|
||||
"title": "Dupliser denne listen",
|
||||
"success": "Liste duplisert.",
|
||||
"text": "Velg et navnerom som skal romme den dupliserte listen:",
|
||||
"label": "Dupliser"
|
||||
},
|
||||
"background": {
|
||||
"poweredByUnsplash": "Kjøres med Unsplash",
|
||||
"removeSuccess": "Bakgrunn fjernet.",
|
||||
"success": "Bakgrunn satt.",
|
||||
"searchPlaceholder": "Søk etter en bakgrunn …",
|
||||
"remove": "Fjern bakgrunn",
|
||||
"title": "Sett listebakgrunn",
|
||||
"loadMore": "Last inn flere bilder",
|
||||
"upload": "Velg en bakgrunn fra PC-en din"
|
||||
},
|
||||
"kanban": {
|
||||
"deleteBucketSuccess": "Bøtte slettet.",
|
||||
"addAnotherTask": "Legg til et annet gjøremål",
|
||||
"addTask": "Legg til et gjøremål",
|
||||
"deleteBucketText1": "Slett bøtten?",
|
||||
"deleteHeaderBucket": "Slett bøtten",
|
||||
"addBucket": "Opprett en ny bøtte",
|
||||
"noLimit": "Ikke satt",
|
||||
"limit": "Grense: {limit}",
|
||||
"doneBucketSavedSuccess": "Utført-bøtte lagret.",
|
||||
"doneBucketHintExtended": "Alle gjøremål flyttet til utført-bøtten vil markeres som utført automatisk. Alle gjøremål markert som utført annensteds fra flyttes også.",
|
||||
"doneBucketHint": "Alle gjøremål flyttet inn i denne bøtten vil automatisk markeres som utførte.",
|
||||
"doneBucket": "Utført-bøtte",
|
||||
"addTaskPlaceholder": "Skriv inn gjøremålsnavn…",
|
||||
"bucketTitleSavedSuccess": "Bøttenavn lagret.",
|
||||
"bucketLimitSavedSuccess": "Bøttegrense lagret.",
|
||||
"addBucketPlaceholder": "Skriv inn nytt bøttenavn …",
|
||||
"deleteLast": "Du kan ikke fjerne den siste bøtten.",
|
||||
"title": "Kanban",
|
||||
"deleteBucketText2": "Dette vil ikke slette noen gjøremål, men snarere flytte dem til forvalgt bøtte."
|
||||
},
|
||||
"share": {
|
||||
"links": {
|
||||
"create": "Opprett en ny lenkedeling",
|
||||
"deleteSuccess": "Lenke slettet",
|
||||
"password": "Passord (valgfritt)",
|
||||
"name": "Navn (valgfritt)",
|
||||
"namePlaceholder": "f.eks. Lorem ipsum",
|
||||
"title": "Lenkedelinger",
|
||||
"explanation": "Lar deg dele en liste til dem uten konto på Vikunja.",
|
||||
"removeText": "Fjern denne lenkedeling? Det vil ikke lenger være mulig å nå denne listen med denne linkedelingen. Dette kan ikke angres!",
|
||||
"remove": "Fjern en lenkedeling",
|
||||
"nameExplanation": "Alle handlinger gjort av denne lenkedelingen vil dukke opp med navnet.",
|
||||
"what": "Hva er en lenkedeling?",
|
||||
"createSuccess": "Lenkedeling opprettet.",
|
||||
"noName": "Navn ikke satt",
|
||||
"passwordExplanation": "Brukeren må skrive inn dette passordet for å logge inn."
|
||||
},
|
||||
"right": {
|
||||
"admin": "Admin",
|
||||
"title": "Høyre",
|
||||
"read": "Skrivebeskyttet",
|
||||
"readWrite": "Lesing og skriving"
|
||||
},
|
||||
"userTeam": {
|
||||
"updatedSuccess": "{type} lagt til.",
|
||||
"addedSuccess": "{type} lagt til.",
|
||||
"removeSuccess": "{sharable} fjernet fra {type}.",
|
||||
"you": "Deg",
|
||||
"removeText": "Fjern denne {sharable} fra {type}? Dette kan ikke angres!",
|
||||
"removeHeader": "Fjern en {type} fra {sharable}",
|
||||
"shared": "Delt med disse {type}",
|
||||
"notShared": "Ikke delt med noe {type} enda.",
|
||||
"typeTeam": "lag | lag",
|
||||
"typeUser": "bruker | brukere"
|
||||
},
|
||||
"title": "Del «{list}»",
|
||||
"header": "Del denne listen",
|
||||
"attributes": {
|
||||
"delete": "Slett",
|
||||
"sharedBy": "Delt av",
|
||||
"name": "Navn",
|
||||
"link": "Lenke",
|
||||
"right": "Rettighet"
|
||||
},
|
||||
"share": "Del"
|
||||
},
|
||||
"edit": {
|
||||
"title": "Rediger «{list}»",
|
||||
"success": "Liste oppdatert.",
|
||||
"description": "Beskrivelse",
|
||||
"header": "Rediger denne listen",
|
||||
"color": "Farge",
|
||||
"identifierTooltip": "Listeidentifikatoren kan brukes for å unikt identifisere gjøremål mellom lister. Du kan la den stå tom for å skru den av.",
|
||||
"descriptionPlaceholder": "Skriv inn listebeskrivelse…",
|
||||
"titlePlaceholder": "Skriv inn listenavn…",
|
||||
"identifierPlaceholder": "Listeidentifikator her …",
|
||||
"identifier": "Listeidentifikator"
|
||||
},
|
||||
"table": {
|
||||
"columns": "Kolonner",
|
||||
"title": "Tabell"
|
||||
},
|
||||
"list": {
|
||||
"title": "Liste",
|
||||
"editTask": "Rediger gjøremål",
|
||||
"newTaskCta": "Opprett et nytt gjøremål.",
|
||||
"addPlaceholder": "Legg til nytt gjøremål …",
|
||||
"add": "Legg til",
|
||||
"addTitleRequired": "Velg et navn.",
|
||||
"empty": "Denne listen er for tiden tom."
|
||||
},
|
||||
"delete": {
|
||||
"success": "Liste slettet.",
|
||||
"title": "Slett «{list}»",
|
||||
"header": "Slett denne listen",
|
||||
"text2": "Dette inkluderer alle gjøremål og kan ikke angres!",
|
||||
"text1": "Slett denne listen og hele dens innhold?"
|
||||
},
|
||||
"archive": {
|
||||
"archive": "Arkiver denne listen",
|
||||
"title": "Arkiver «{list}»",
|
||||
"success": "Liste arkivert.",
|
||||
"unarchive": "Av-arkiver denne listen",
|
||||
"archiveText": "Du vil ikke kunne redigere denne listen eller opprette nye gjøremål før du av-arkiverer den.",
|
||||
"unarchiveText": "Du vil ikke kunne opprette nye gjøremål eller redigere den."
|
||||
},
|
||||
"create": {
|
||||
"createdSuccess": "Liste opprettet.",
|
||||
"addTitleRequired": "Angi en tittel.",
|
||||
"header": "Opprett en ny liste",
|
||||
"titlePlaceholder": "Skriv inn listenavn…"
|
||||
},
|
||||
"shared": "Delte lister",
|
||||
"search": "Skriv for å søke etter en liste …",
|
||||
"lists": "Lister",
|
||||
"color": "Farge",
|
||||
"gantt": {
|
||||
"to": "Til",
|
||||
"from": "Fra",
|
||||
"day": "Dag",
|
||||
"month": "Måned",
|
||||
"size": "Størrelse",
|
||||
"title": "Gantt-skjema",
|
||||
"noDates": "Dette gjøremålet har ingen satte datoer.",
|
||||
"default": "Forvalg",
|
||||
"showTasksWithoutDates": "Vis gjøremål som ikke har satte datoer enda"
|
||||
},
|
||||
"title": "Listens navn",
|
||||
"searchSelect": "Klikk eller trykk Enter for å velge denne listen",
|
||||
"archived": "Denne listen er arkivert. Det er ikke mulig å opprette nye eller redigere gjøremål i den."
|
||||
},
|
||||
"user": {
|
||||
"settings": {
|
||||
"totp": {
|
||||
"disable": "Skru av to-faktorbekreftelse",
|
||||
"confirmSuccess": "TOTP-oppsett bekreftet, og klar til bruk.",
|
||||
"enterPassword": "Skriv inn passordet ditt",
|
||||
"scanQR": "Alternativt kan du skanne denne QR-koden:",
|
||||
"finishSetupPart1": "For å fullføre oppsettet, bruker du denne hemmeligheten i ditt TOTP-program (andOTP eller lignende):",
|
||||
"setupSuccess": "Du har nå satt opp to-faktorbekreftelse.",
|
||||
"passcode": "Kode",
|
||||
"finishSetupPart2": "Etter det skriver du inn en kode fra programmet ditt nedenfor.",
|
||||
"disableSuccess": "To-faktorbekreftelse av.",
|
||||
"passcodePlaceholder": "En kode generert av ditt TOTP-program",
|
||||
"title": "To-faktorbekreftelse",
|
||||
"enroll": "Bruk"
|
||||
},
|
||||
"avatar": {
|
||||
"statusUpdateSuccess": "Avatarstatus oppdatert.",
|
||||
"uploadAvatar": "Last opp avatar",
|
||||
"initials": "Initialer",
|
||||
"title": "Avatar",
|
||||
"gravatar": "Gravatar",
|
||||
"upload": "Last opp",
|
||||
"setSuccess": "Avatar satt."
|
||||
},
|
||||
"caldav": {
|
||||
"more": "Mer info om CalDAV i Vikunja",
|
||||
"title": "CalDAV",
|
||||
"howTo": "Du kan koble Vikunja til CalDAV-klienter for å vise og håndtere alle gjøremål fra forskjellige klienter. Skriv inn denne nettadressen i klienten din:"
|
||||
},
|
||||
"general": {
|
||||
"emailReminders": "Send meg påminnelser om gjøremål per e-post",
|
||||
"savedSuccess": "Innstillinger oppdatert.",
|
||||
"language": "Språk",
|
||||
"weekStartMonday": "Mandag",
|
||||
"weekStartSunday": "Søndag",
|
||||
"weekStart": "Ukestart",
|
||||
"newName": "Det nye navnet",
|
||||
"name": "Navn",
|
||||
"title": "Generelle innstillinger",
|
||||
"playSoundWhenDone": "Spill lyd når gjøremål markeres som utført",
|
||||
"discoverableByEmail": "La andre finne meg ved å søke etter hele e-postadressen min",
|
||||
"discoverableByName": "La andre finne meg ved å søke etter hele navnet mitt",
|
||||
"overdueReminders": "Send meg påminnelser om gjøremål med utløpt frist via e-post hver morgen"
|
||||
},
|
||||
"passwordsDontMatch": "Nytt passord og bekreftelsen av det samsvarer ikke.",
|
||||
"passwordUpdateSuccess": "Passord oppdatert.",
|
||||
"updateEmailNew": "Ny e-postadresse",
|
||||
"updateEmailTitle": "Oppdater din e-postadresse",
|
||||
"currentPasswordPlaceholder": "Ditt nåværende passord",
|
||||
"currentPassword": "Nåværende",
|
||||
"newPasswordConfirm": "Bekreft nytt passord",
|
||||
"newPassword": "Nytt passord",
|
||||
"title": "Innstillinger",
|
||||
"newPasswordTitle": "Oppdater passordet ditt",
|
||||
"updateEmailSuccess": "E-postadresse oppdatert. Klikk lenken i e-posten sendt til deg for å bekrefte den."
|
||||
},
|
||||
"auth": {
|
||||
"confirmEmailSuccess": "Du kan logge inn med din e-postadresse nå.",
|
||||
"totpPlaceholder": "f.eks. 123456",
|
||||
"emailPlaceholder": "f.eks. fredrik@vikunja.io",
|
||||
"usernamePlaceholder": "f.eks. Fredrik",
|
||||
"passwordPlaceholder": "f.eks. •••••••••••",
|
||||
"logout": "Logg ut",
|
||||
"loginWith": "Logg inn med {provider}",
|
||||
"passwordsDontMatch": "Passordene samsvarer ikke",
|
||||
"resetPassword": "Tilbakestill passordet ditt",
|
||||
"passwordRepeat": "Skriv inn passordet igjen",
|
||||
"password": "Passord",
|
||||
"email": "E-postadresse",
|
||||
"usernameEmail": "Brukernavn eller e-postadresse",
|
||||
"username": "Brukernavn",
|
||||
"authenticating": "Identitetsbekrefter …",
|
||||
"register": "Registrer deg",
|
||||
"login": "Logg inn",
|
||||
"resetPasswordSuccess": "Sjekk innboksen din for e-post med instruks om tilbakestilling av passord.",
|
||||
"resetPasswordAction": "Send lenke for tilbakestilling av passord.",
|
||||
"openIdStateError": "Tilstanden stemmer ikke overens. Kan ikke fortsette.",
|
||||
"totpTitle": "Kode fra to-faktorbekreftelse"
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
"attributes": {
|
||||
"member": "Medlem",
|
||||
"description": "Beskrivelse",
|
||||
"nameRequired": "Angi et navn.",
|
||||
"namePlaceholder": "Skriv inn lagnavn …",
|
||||
"admin": "Admin",
|
||||
"name": "Lagnavn"
|
||||
},
|
||||
"edit": {
|
||||
"deleteUser": {
|
||||
"header": "Fjern en bruker fra laget",
|
||||
"success": "Bruker fjernet fra laget.",
|
||||
"text2": "Brukeren vil miste tilgang til alle lister og navnerom dette laget har tilgang til. Dette kan ikke angres!",
|
||||
"text1": "Fjern denne brukeren fra laget?"
|
||||
},
|
||||
"members": "Lagmedlemmer",
|
||||
"delete": {
|
||||
"success": "Lag slettet.",
|
||||
"header": "Slett laget",
|
||||
"text2": "Et lagmedlem vil miste tilgang til lister og navnerom delt med dette laget. Dette kan ikke angres!",
|
||||
"text1": "Slett dette laget og alle dets medlemmer?"
|
||||
},
|
||||
"search": "Skriv for å søke etter bruker…",
|
||||
"madeAdmin": "Lagmedlem gjort til admin.",
|
||||
"madeMember": "Lagmedlem gjort til medlem.",
|
||||
"userAddedSuccess": "Lagmedlem lagt til.",
|
||||
"success": "Lag oppdatert.",
|
||||
"title": "Rediger «{team}»-laget",
|
||||
"makeAdmin": "Gjør til admin",
|
||||
"makeMember": "Gjør til medlem",
|
||||
"addUser": "Legg til i lag"
|
||||
},
|
||||
"create": {
|
||||
"success": "Lag opprettet.",
|
||||
"title": "Opprett et nytt lag"
|
||||
},
|
||||
"noTeams": "Du er ikke del av noen lag.",
|
||||
"title": "Lag"
|
||||
},
|
||||
"task": {
|
||||
"priority": {
|
||||
"medium": "Middels",
|
||||
"low": "Lav",
|
||||
"doNow": "GJØR NÅ",
|
||||
"urgent": "Haster",
|
||||
"high": "Høy",
|
||||
"unset": "Ikke satt"
|
||||
},
|
||||
"label": {
|
||||
"removeSuccess": "Etikett fjernet.",
|
||||
"createSuccess": "Etikett opprettet.",
|
||||
"addSuccess": "Etikett lagt til.",
|
||||
"createPlaceholder": "Legg til dette som en ny etikett",
|
||||
"placeholder": "Skriv for å legge til en ny etikett …"
|
||||
},
|
||||
"detail": {
|
||||
"actions": {
|
||||
"label": "Legg til etiketter",
|
||||
"delete": "Slett gjøremål",
|
||||
"moveList": "Flytt gjøremål",
|
||||
"attachments": "Legg til vedlegg",
|
||||
"repeatAfter": "Sett gjentagelsesintervaller",
|
||||
"reminders": "Sett påminnelser",
|
||||
"endDate": "Sett en sluttdato",
|
||||
"startDate": "Sett startdato",
|
||||
"dueDate": "Sett fristdato",
|
||||
"priority": "Sett prioritet",
|
||||
"percentDone": "Sett % utført",
|
||||
"assign": "Tildel en bruker dette gjøremålet",
|
||||
"color": "Sett gjøremålsfarge",
|
||||
"relatedTasks": "Legg til gjøremålsrelasjoner"
|
||||
},
|
||||
"deleteSuccess": "Gjøremål slettet.",
|
||||
"doneAt": "Utført {0}",
|
||||
"updated": "Oppdatert {0}",
|
||||
"created": "Opprettet {0} av {1}",
|
||||
"chooseEndDate": "Klikk her for å sette en sluttdato",
|
||||
"chooseStartDate": "Klikk her for å sette en startdato",
|
||||
"chooseDueDate": "Klikk her for å sette en fristdato",
|
||||
"delete": {
|
||||
"header": "Slett dette gjøremålet",
|
||||
"text2": "Dette fil også fjerne alle vedlegg, påminnelser og relasjoner tilknyttet dette gjøremålet, og kan ikke angres!",
|
||||
"text1": "Fjern dette gjøremålet?"
|
||||
},
|
||||
"updateSuccess": "Gjøremål lagret.",
|
||||
"done": "Utført!",
|
||||
"belongsToList": "Dette gjøremålet tilhører «{list}»-listen",
|
||||
"due": "Frist {at}",
|
||||
"undone": "Marker som uferdig",
|
||||
"move": "Flytt gjøremål til en annen liste"
|
||||
},
|
||||
"show": {
|
||||
"today": "I dag",
|
||||
"from": "Gjøremål fra",
|
||||
"current": "Nåværende gjøremål",
|
||||
"noDates": "Vis gjøremål uten datoer",
|
||||
"titleCurrent": "Nåværende gjøremål",
|
||||
"nextMonth": "Neste måned",
|
||||
"nextWeek": "Neste uke",
|
||||
"noTasks": "Ingenting fore. Ha en fin dag.",
|
||||
"until": "til",
|
||||
"titleDates": "Gjøremål fra {from} til {to}"
|
||||
},
|
||||
"openDetail": "Åpne gjøremålets detaljvisning",
|
||||
"undoneSuccess": "Gjøremål avmarkert som utført.",
|
||||
"doneSuccess": "Gjøremål markert som utført.",
|
||||
"addReminder": "Legg til ny påminnelse …",
|
||||
"comment": {
|
||||
"edited": "redigert {date}",
|
||||
"loading": "Laster inn kommentarer …",
|
||||
"title": "Kommentarer",
|
||||
"addedSuccess": "Kommentar lagt til.",
|
||||
"deleteText2": "Dette kan ikke angres.",
|
||||
"deleteText1": "Slett denne kommentaren?",
|
||||
"delete": "Slett denne kommentaren",
|
||||
"comment": "Kommentar",
|
||||
"placeholder": "Legg til din kommentar …",
|
||||
"creating": "Kommenterer …"
|
||||
},
|
||||
"attachment": {
|
||||
"delete": "Slett vedlegg",
|
||||
"title": "Vedlegg",
|
||||
"deleteText2": "Dette kan ikke angres.",
|
||||
"deleteText1": "Slett {filename}-vedlegget?",
|
||||
"drop": "Slipp filer her for å laste opp",
|
||||
"upload": "Last opp vedlegg",
|
||||
"download": "Last ned",
|
||||
"createdBy": "opprettet {0} av {1}"
|
||||
},
|
||||
"attributes": {
|
||||
"startDate": "Startdato",
|
||||
"reminders": "Påminnelser",
|
||||
"repeat": "Gjenta",
|
||||
"relatedTasks": "Relaterte gjøremål",
|
||||
"priority": "Prioritet",
|
||||
"percentDone": "% utført",
|
||||
"labels": "Etiketter",
|
||||
"endDate": "Sluttdato",
|
||||
"dueDate": "Fristdato",
|
||||
"done": "Ferdig",
|
||||
"description": "Beskrivelse",
|
||||
"createdBy": "Opprettet av",
|
||||
"created": "Opprettet",
|
||||
"color": "Farge",
|
||||
"assignees": "Tildelte",
|
||||
"updated": "Oppdatert",
|
||||
"title": "Navn"
|
||||
},
|
||||
"delete": "Slett dette gjøremålet",
|
||||
"new": "Opprett et nytt gjøremål",
|
||||
"repeat": {
|
||||
"years": "År",
|
||||
"months": "Måneder",
|
||||
"weeks": "Uker",
|
||||
"days": "Dager",
|
||||
"hours": "Timer",
|
||||
"specifyAmount": "Angi mengde…",
|
||||
"fromCurrentDate": "Fra nåværende dato",
|
||||
"monthly": "Månedlig",
|
||||
"everyMonth": "Hver måned",
|
||||
"everyWeek": "Hver uke",
|
||||
"everyDay": "Hver dag",
|
||||
"mode": "Gjentagelsesmodus",
|
||||
"each": "Hver"
|
||||
},
|
||||
"relation": {
|
||||
"new": "Ny gjøremålsrelasjon",
|
||||
"noneYet": "Ingen gjøremålsrelasjoner enda.",
|
||||
"deleteText2": "Dette kan ikke angres!",
|
||||
"deleteText1": "Slett denne gjøremålstilknytningen?",
|
||||
"differentList": "Dette gjøremålet tilhører en annen liste.",
|
||||
"createPlaceholder": "Legg til dette som nytt relatert gjøremål",
|
||||
"searchPlaceholder": "Skriv for å søke etter et nytt gjøremål å legge til som relatert …",
|
||||
"delete": "Slett gjøremålsrelasjon",
|
||||
"add": "Legg til ny gjøremålsrelasjon"
|
||||
},
|
||||
"deferDueDate": {
|
||||
"1week": "1 uke",
|
||||
"3days": "3 dager",
|
||||
"1day": "1 dag"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedThroughParent": "Du kan ikke oppheve abonnement her fordi du ikke har abonnert på denne {entity} gjennom dens {parent}.",
|
||||
"unsubscribeSuccess": "Du har opphevet abonnementet på denne {entity}",
|
||||
"subscribeSuccess": "Du abonnerer nå på denne {entity}",
|
||||
"unsubscribe": "Opphev abonnement",
|
||||
"subscribe": "Abonner",
|
||||
"subscribed": "Du abonnerer på denne {entity} og vil motta merkander om endringer.",
|
||||
"notSubscribed": "Du abonnerer ikke på denne {entity} og vil ikke motta merknader om endringer."
|
||||
},
|
||||
"assignee": {
|
||||
"unassignSuccess": "Brukertilknytning opphevet.",
|
||||
"assignSuccess": "Bruker tilknyttet.",
|
||||
"selectPlaceholder": "Tilknytt denne brukeren",
|
||||
"placeholder": "Skriv for å tilknytte en bruker …"
|
||||
},
|
||||
"description": {
|
||||
"empty": "Ingen beskrivelse tilgjengelig enda.",
|
||||
"placeholder": "Klikk her for å legge til en beskrivelse …"
|
||||
},
|
||||
"createSuccess": "Gjøremål opprettet.",
|
||||
"task": "Gjøremål"
|
||||
},
|
||||
"label": {
|
||||
"attributes": {
|
||||
"description": "Beskrivelse",
|
||||
"color": "Farge",
|
||||
"descriptionPlaceholder": "Etikettbeskrivelse",
|
||||
"titlePlaceholder": "Skriv inn et etikettnavn…",
|
||||
"title": "Navn"
|
||||
},
|
||||
"deleteSuccess": "Etikett slettet.",
|
||||
"edit": {
|
||||
"success": "Etikett oppdatert.",
|
||||
"header": "Rediger etikett",
|
||||
"forbidden": "Du kan ikke redigere denne etiketten fordi du ikke eier den."
|
||||
},
|
||||
"create": {
|
||||
"title": "Opprett en ny etikett",
|
||||
"header": "Ny etikett",
|
||||
"success": "Etikett opprettet.",
|
||||
"titleRequired": "Angi et navn."
|
||||
},
|
||||
"manage": "Håndter etiketter",
|
||||
"title": "Etiketter",
|
||||
"description": "Klikk på en etikett for å redigere den. Du kan redigere alle dem du har opprettet, og du kan bruke etiketter fra alle fra gjøremål du har tilgang til.",
|
||||
"search": "Skriv for å søke etter en etikett …",
|
||||
"newCTA": "Du har for øyeblikket ingen etiketter."
|
||||
},
|
||||
"filters": {
|
||||
"create": {
|
||||
"action": "Opprett nytt lagret filter",
|
||||
"title": "Opprett et lagret filter"
|
||||
},
|
||||
"edit": {
|
||||
"success": "Filter lagret.",
|
||||
"title": "Rediger dette lagrede filteret"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Filter slettet.",
|
||||
"text": "Er du sikker på at du ønsker å slette dette lagrede filteret?",
|
||||
"header": "Slett dette lagrede filteret"
|
||||
},
|
||||
"attributes": {
|
||||
"description": "Beskrivelse",
|
||||
"requireAll": "Krev alle filter for at et gjøremål skal vises",
|
||||
"showDoneTasks": "Vis utførte gjøremål",
|
||||
"descriptionPlaceholder": "Skriv inn en beskrivelse…",
|
||||
"includeNulls": "Inkluder gjøremål uten verdier",
|
||||
"titlePlaceholder": "Skriv inn et lagret filternavn…",
|
||||
"title": "Navn",
|
||||
"dueDateRange": "Fristdato-område",
|
||||
"startDateRange": "Startdato-område",
|
||||
"reminderRange": "Påminnelsesdato-område",
|
||||
"endDateRange": "Sluttdato-område",
|
||||
"enablePercentDone": "Filtrer etter fullførelse",
|
||||
"enablePriority": "Filtrer etter prioritet"
|
||||
},
|
||||
"title": "Filter"
|
||||
},
|
||||
"misc": {
|
||||
"searchPlaceholder": "Skriv for å søke …",
|
||||
"disable": "Skru av",
|
||||
"saving": "Lagrer …",
|
||||
"create": "Opprett",
|
||||
"info": "Info",
|
||||
"next": "Neste",
|
||||
"previous": "Forrige",
|
||||
"search": "Søk",
|
||||
"copy": "Kopier til utklippstavle",
|
||||
"cancel": "Avbryt",
|
||||
"confirm": "Bekreft",
|
||||
"delete": "Slett",
|
||||
"save": "Lagre",
|
||||
"loading": "Laster inn …",
|
||||
"default": "Forvalg",
|
||||
"poweredBy": "Drevet av Vikunja",
|
||||
"refresh": "Gjenoppfrisk",
|
||||
"doit": "Gjør det!",
|
||||
"saved": "Lagret!"
|
||||
},
|
||||
"navigation": {
|
||||
"settings": "Innstillinger",
|
||||
"overview": "Oversikt",
|
||||
"privacy": "Personvernspraksis",
|
||||
"imprint": "Imprint",
|
||||
"upcoming": "Kommende"
|
||||
},
|
||||
"migrate": {
|
||||
"import": "Importer din data inn i Vikunja",
|
||||
"getStarted": "Begynn",
|
||||
"titleService": "Importer din data fra {name} inn i Vikunja",
|
||||
"confirm": "Jeg er sikker. Begynn migreringen nå.",
|
||||
"alreadyMigrated1": "Det ser ut til at du allerede har importert ting fra {name} den {date}.",
|
||||
"authorize": "Innvilg Vikunja tilgang til din {name}-konto ved å klikke på knappen nedenfor.",
|
||||
"description": "Klikk på logoen til én av tredjepartstjenestene nedenfor for å begynne.",
|
||||
"inProgress": "Importerer …",
|
||||
"descriptionDo": "Vikunja vil importere alle lister, gjøremål, notater, påminnelser og filer du har tilgang til."
|
||||
},
|
||||
"404": {
|
||||
"text": "Siden du forespurte finnes ikke.",
|
||||
"title": "Ikke funnet"
|
||||
},
|
||||
"home": {
|
||||
"list": {
|
||||
"importText": "Eller importer lister og gjøremål fra andre tjenester inn i Vikunja:",
|
||||
"import": "Importer din data inn i Vikunja",
|
||||
"new": "Opprett en ny liste",
|
||||
"newText": "Du kan opprette en ny liste for dine nye gjøremål:"
|
||||
},
|
||||
"welcome": "Hei {username}"
|
||||
},
|
||||
"input": {
|
||||
"multiselect": {
|
||||
"createPlaceholder": "Opprett ny",
|
||||
"selectPlaceholder": "Klikk eller trykk Enter for å velge"
|
||||
},
|
||||
"editor": {
|
||||
"table": "Tabell",
|
||||
"image": "Bilde",
|
||||
"link": "Lenke",
|
||||
"quote": "Sitat",
|
||||
"code": "Kode",
|
||||
"heading3": "Overskrift 3",
|
||||
"heading2": "Overskrift 2",
|
||||
"heading1": "Overskrift 1",
|
||||
"done": "Utført",
|
||||
"strikethrough": "Gjennomstreket",
|
||||
"italic": "Kursiv",
|
||||
"cleanBlock": "Tom blokk",
|
||||
"sideBySide": "Side-ved-side",
|
||||
"horizontalRule": "Vannrett linje",
|
||||
"headingBigger": "Større overskrift",
|
||||
"headingSmaller": "Mindre overskrift",
|
||||
"guide": "Veiledning",
|
||||
"orderedList": "Sortert liste",
|
||||
"unorderedList": "Usortert liste",
|
||||
"bold": "Fet"
|
||||
},
|
||||
"datepicker": {
|
||||
"chooseDate": "Velg en dato",
|
||||
"nextWeek": "Neste uke",
|
||||
"laterThisWeek": "Senere denne uken",
|
||||
"nextMonday": "Neste mandag",
|
||||
"tomorrow": "I morgen",
|
||||
"today": "I dag",
|
||||
"thisWeekend": "Denne helgen"
|
||||
},
|
||||
"resetColor": "Tilbakestill farge"
|
||||
},
|
||||
"apiConfig": {
|
||||
"change": "endre",
|
||||
"url": "Vikunja-nettadresse",
|
||||
"success": "Bruker Vikunja-installasjonen på «{domain}».",
|
||||
"error": "Fant ingen brukbar Vikunja-installasjon på «{domain}».",
|
||||
"urlPlaceholder": "f.eks: https://lokalvert:3456",
|
||||
"signInOn": "Logg inn på din Vikunja-konto på {0}"
|
||||
},
|
||||
"menu": {
|
||||
"newList": "Ny liste",
|
||||
"share": "Del",
|
||||
"delete": "Slett",
|
||||
"edit": "Rediger",
|
||||
"setBackground": "Sett bakgrunn",
|
||||
"duplicate": "Dupliser",
|
||||
"unarchive": "Av-arkiver",
|
||||
"archive": "Akriver"
|
||||
},
|
||||
"sharing": {
|
||||
"invalidPassword": "Passordet er ugyldig.",
|
||||
"passwordRequired": "Den delte listen krever et passord. Skriv det inn nedenfor:",
|
||||
"authenticating": "Identitetsbekrefter…",
|
||||
"error": "En feil oppstod."
|
||||
},
|
||||
"date": {
|
||||
"altFormatLong": "j M Y H:i",
|
||||
"altFormatShort": "j M Y",
|
||||
"ago": "{date} siden",
|
||||
"locale": "nb_NO",
|
||||
"in": "den {date}"
|
||||
},
|
||||
"quickActions": {
|
||||
"hint": "Du kan bruke «#» for å kun søke etter gjøremål, «*». for å kun søke etter lister, og «@» for å kun søke etter lag.",
|
||||
"newTask": "Skriv inn gjøremålsnavn…",
|
||||
"newNamespace": "Skriv inn navneromsnavn…",
|
||||
"createList": "Opprett en liste i nåværende navnerom ({title})",
|
||||
"cmds": {
|
||||
"newTeam": "Nytt lag",
|
||||
"newNamespace": "Nytt navnerom",
|
||||
"newList": "Ny liste",
|
||||
"newTask": "Nytt gjøremål"
|
||||
},
|
||||
"newTeam": "Skriv inn navnet på det nye laget …",
|
||||
"teams": "Lag",
|
||||
"lists": "Lister",
|
||||
"tasks": "Gjøremål",
|
||||
"placeholder": "Skriv en kommando, eller søk …",
|
||||
"commands": "Kommandoer",
|
||||
"createTask": "Opprett et gjøremål i nåværende liste ({title})",
|
||||
"newList": "Skriv inn nytt listenavn…"
|
||||
},
|
||||
"notification": {
|
||||
"explainer": "Merknader vil vises her når handlinger på navnerom, lister, eller gjøremål du abonnerer på finner sted.",
|
||||
"none": "Ingen merknader. Ha en fin dag."
|
||||
},
|
||||
"error": {
|
||||
"5012": "Navnerommet er arkivert og kan derfor kun leses.",
|
||||
"5009": "Lesetilgang til navnerom kreves for å utføre denne handlingen.",
|
||||
"10004": "Du kan ikke legge til gjøremålet som bøtte, siden den allerede overskrider grenser for gjøremål den kan holde.",
|
||||
"12002": "Du abonnerer allerede på selve oppføringen, eller en ovennevnt oppføring.",
|
||||
"4018": "Ugyldig gjøremålsfilter-sammentrekning.",
|
||||
"4017": "Ugyldig gjøremålsfilter-sammenligner.",
|
||||
"4005": "Du mangler rettigheten til å se dette gjøremålet.",
|
||||
"4004": "Må ha minst ett gjøremål ved masseredigering av gjøremål.",
|
||||
"4003": "Alle masse-redigeringsgjøremål må tilhøre samme liste.",
|
||||
"0001": "Du tillates ikke å gjøre dette.",
|
||||
"9001": "Rettigheten er ugyldig.",
|
||||
"4013": "Gjøremålssorteringsparameteret er ugyldig.",
|
||||
"4007": "Du kan ikke opprette en gjøremålsrelasjon med ugyldig relasjonstype.",
|
||||
"4006": "Du kan ikke sette et ovennevnt gjøremål som selve gjøremålet.",
|
||||
"3004": "Du må ha lesetilgang til den listen for å utføre denne handlingen.",
|
||||
"1012": "Brukerens e-postadresse er ubekreftet.",
|
||||
"1018": "Brukeravatar-innstillingen er ugyldig.",
|
||||
"6005": "Brukeren er allerede medlem av det laget.",
|
||||
"6004": "Dette lagmedlemmet har allerede tilgang til listen eller navnerommet.",
|
||||
"6002": "Dette lagmedlemmet finnes ikke.",
|
||||
"4009": "Gjøremålsrelasjonen finnes ikke.",
|
||||
"4008": "Du kan ikke opprette en gjøremålsrelasjon som allerede finnes.",
|
||||
"2002": "Noe av forespørselsdataen var ugyldig.",
|
||||
"13001": "Påkrevd passord ble ikke angitt for denne lenkedelingen.",
|
||||
"13002": "Ugyldig lenkedelingspassord.",
|
||||
"4002": "Listegjøremålet finnes ikke.",
|
||||
"1017": "Ugyldig TOTP-kode.",
|
||||
"1016": "Denne brukeren bruker TOTP.",
|
||||
"1015": "Denne brukeren bruker allerede TOTP.",
|
||||
"1014": "Det gamle passordet er tomt.",
|
||||
"1013": "Det nye passordet er tomt.",
|
||||
"1010": "Ugyldig symbol for e-postbekreftelse.",
|
||||
"1009": "Ugyldig symbol for passordtilbakestilling.",
|
||||
"1006": "Kunne ikke hente den bruker-ID-en.",
|
||||
"10005": "Det kan kun være én utført-bøtte per liste.",
|
||||
"3007": "En liste med denne identifikatoren finnes allerede.",
|
||||
"3006": "Lenkedelingen finnes ikke.",
|
||||
"3005": "Du må skrive inn et listenavn.",
|
||||
"4016": "Ugyldig gjøremålsfelt.",
|
||||
"4015": "Gjøremålskommentaren finnes ikke.",
|
||||
"4014": "Gjøremålets sorteringsrekkefølge er ugyldig.",
|
||||
"4012": "Gjøremålsvedlegget er for stort.",
|
||||
"4011": "Gjøremålsvedlegget finnes ikke.",
|
||||
"1005": "Brukeren finnes ikke.",
|
||||
"1004": "Brukernavn og passord ikke angitt.",
|
||||
"1002": "En bruker med den e-postadressen finnes allerede.",
|
||||
"1001": "En bruker med det brukernavnet finnes allerede.",
|
||||
"success": "Vellykket",
|
||||
"11002": "Lagrede filter er ikke tilgjengelige for lenkedelinger.",
|
||||
"11001": "Lagret filter finnes ikke.",
|
||||
"10003": "Du kan ikke fjerne den siste bøtten i en liste.",
|
||||
"10002": "Bøtten hører ikke til den listen.",
|
||||
"10001": "Denne bøtten finnes ikke.",
|
||||
"8003": "Du har ikke tilgang til denne etiketten.",
|
||||
"8002": "Denne etiketten finnes ikke.",
|
||||
"8001": "Denne etiketten finnes allerede for dette gjøremålet.",
|
||||
"7003": "Du har ikke tilgang til den listen.",
|
||||
"7002": "Brukeren har allerede tilgang til den listen.",
|
||||
"3001": "Listen finnes ikke.",
|
||||
"2001": "ID kan ikke være tom eller 0.",
|
||||
"1011": "Feil brukernavn eller passord.",
|
||||
"error": "Feil",
|
||||
"12001": "Abonnementenhetstypen er ugyldig.",
|
||||
"5010": "Dette laget har ikke tilgang til det navnerommet.",
|
||||
"5006": "Navneromsnavnet kan ikke stå tomt.",
|
||||
"5003": "Du har ikke tilgang til angitt navnerom.",
|
||||
"5001": "Navnerommet finnes ikke.",
|
||||
"4019": "Ugyldig gjøremålsfilterverdi.",
|
||||
"4010": "Kan ikke relatere gjøremål til seg selv.",
|
||||
"1008": "Symbol for passordtilbakestilling ble ikke angitt.",
|
||||
"4001": "Listegjøremålsteksten kan ikke være tom.",
|
||||
"3008": "Denne arkiverte listen og alle tilknyttede gjøremål kan kun leses.",
|
||||
"6001": "Skriv inn et lagnavn først.",
|
||||
"5011": "Denne brukeren har allerede tilgang til det navnerommet.",
|
||||
"6007": "Laget har ikke tilgang til listen for å utføre den handlingen.",
|
||||
"6006": "Kan ikke slette siste lagmedlem."
|
||||
},
|
||||
"loadingError": {
|
||||
"failed": "Innlasting feilet, {0}. Hvis feilen vedvarer, {1}.",
|
||||
"tryAgain": "prøv igjen",
|
||||
"contact": "kontakt oss"
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"task": {
|
||||
"done": "Marker gjøremål som utført",
|
||||
"related": "Endre relaterte gjøremål for dette gjøremålet",
|
||||
"attachment": "Legg til et vedlegg for dette gjøremålet",
|
||||
"dueDate": "Endre dette gjøremålets fristdato",
|
||||
"labels": "Legg til etiketter for dette gjøremålet",
|
||||
"title": "Gjøremålsside",
|
||||
"assign": "Tildel en bruker dette gjøremålet"
|
||||
},
|
||||
"toggleMenu": "Veksle menyvisning",
|
||||
"currentPageOnly": "Fungerer kun på nåværende side.",
|
||||
"allPages": "Fungerer på alle sider.",
|
||||
"title": "Tastatursnarveier",
|
||||
"quickSearch": "Åpne søk-/hurtighandlingsfeltet"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -1 +1 @@
|
|||
{}
|
||||
{}
|
||||
|
|
|
@ -1 +1,722 @@
|
|||
{}
|
||||
{
|
||||
"home": {
|
||||
"welcome": "Привет, {username}",
|
||||
"list": {
|
||||
"import": "Импорт данных в Vikunja",
|
||||
"importText": "Или импортировать списки и задачи из других сервисов в Vikunja:",
|
||||
"new": "Создать новый список",
|
||||
"newText": "Ты можешь создать новый список для своих задач:"
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"attributes": {
|
||||
"color": "Цвет",
|
||||
"descriptionPlaceholder": "Описание метки",
|
||||
"description": "Описание",
|
||||
"titlePlaceholder": "Введи название метки…",
|
||||
"title": "Название"
|
||||
},
|
||||
"deleteSuccess": "Метка удалена.",
|
||||
"edit": {
|
||||
"success": "Метка изменена.",
|
||||
"forbidden": "Ты не можешь изменять эту метку, потому что не являешься её владельцем.",
|
||||
"header": "Изменить метку"
|
||||
},
|
||||
"create": {
|
||||
"success": "Метка успешно создана.",
|
||||
"titleRequired": "Укажи название.",
|
||||
"title": "Создать новую метку",
|
||||
"header": "Новая метка"
|
||||
},
|
||||
"newCTA": "Меток сейчас нет.",
|
||||
"description": "Нажми на метку, чтобы изменить её. Ты можешь редактировать все созданные тобой метки, ты можешь использовать все метки, связанные с задачей, к списку которой у тебя есть доступ.",
|
||||
"manage": "Управление метками",
|
||||
"title": "Метки",
|
||||
"search": "Введи запрос для поиска метки…"
|
||||
},
|
||||
"filters": {
|
||||
"title": "Фильтры",
|
||||
"edit": {
|
||||
"success": "Фильтр сохранён."
|
||||
},
|
||||
"attributes": {
|
||||
"showDoneTasks": "Показывать завершённые задачи",
|
||||
"descriptionPlaceholder": "Введи описание…",
|
||||
"description": "Описание",
|
||||
"title": "Название",
|
||||
"reminderRange": "Диапазон даты напоминания",
|
||||
"endDateRange": "Диапазон даты завершения",
|
||||
"startDateRange": "Диапазон даты начала",
|
||||
"dueDateRange": "Диапазон срока",
|
||||
"enablePercentDone": "По % завершения",
|
||||
"enablePriority": "Вкл. фильтр по приоритету"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Фильтр удалён."
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"kanban": {
|
||||
"addAnotherTask": "Добавить ещё одну задачу",
|
||||
"addTask": "Добавить задачу",
|
||||
"noLimit": "не установлен",
|
||||
"limit": "Лимит: {limit}",
|
||||
"title": "Канбан",
|
||||
"bucketLimitSavedSuccess": "Ограничение колонки сохранено.",
|
||||
"bucketTitleSavedSuccess": "Название колонки сохранено.",
|
||||
"deleteBucketSuccess": "Колонка удалена.",
|
||||
"deleteBucketText2": "Это не удалит задачи, а переместит их в колонку по умолчанию.",
|
||||
"deleteBucketText1": "Удалить эту колонку?",
|
||||
"deleteHeaderBucket": "Удалить колонку",
|
||||
"addBucketPlaceholder": "Введи название новой колонки…",
|
||||
"addBucket": "Создать новую колонку",
|
||||
"deleteLast": "Нельзя удалить последнюю колонку.",
|
||||
"doneBucketHint": "Все задачи, помещённые в эту колонку, автоматически помечаются завершёнными.",
|
||||
"addTaskPlaceholder": "Введи название задачи…"
|
||||
},
|
||||
"table": {
|
||||
"columns": "Столбцы",
|
||||
"title": "Таблица"
|
||||
},
|
||||
"gantt": {
|
||||
"noDates": "В этой задаче нет установленной даты.",
|
||||
"day": "День",
|
||||
"month": "Месяц",
|
||||
"default": "По умолчанию",
|
||||
"size": "Размер",
|
||||
"showTasksWithoutDates": "Показать задачи без установленной даты"
|
||||
},
|
||||
"list": {
|
||||
"editTask": "Изменить задачу",
|
||||
"newTaskCta": "Создать новую задачу.",
|
||||
"empty": "Список сейчас пуст.",
|
||||
"addTitleRequired": "Укажи имя.",
|
||||
"addPlaceholder": "Добавить новую задачу…",
|
||||
"add": "Добавить",
|
||||
"title": "Список"
|
||||
},
|
||||
"edit": {
|
||||
"success": "Список обновлён.",
|
||||
"color": "Цвет",
|
||||
"descriptionPlaceholder": "Введи описание списка…",
|
||||
"description": "Описание",
|
||||
"identifierPlaceholder": "Идентификатор списка здесь…",
|
||||
"identifier": "Идентификатор списка",
|
||||
"titlePlaceholder": "Введи название списка…",
|
||||
"title": "Изменить «{list}»",
|
||||
"header": "Изменить этот список"
|
||||
},
|
||||
"duplicate": {
|
||||
"success": "Копия списка создана.",
|
||||
"label": "Создать копию",
|
||||
"title": "Создать копию списка",
|
||||
"text": "Выбери пространство имён, в которое поместить копию списка:"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Список удалён.",
|
||||
"text2": "Это включает в себя все задачи, и отменить это будет нельзя!",
|
||||
"text1": "Удалить этот список вместе со всем содержимым?",
|
||||
"header": "Удалить этот список",
|
||||
"title": "Удалить «{list}»"
|
||||
},
|
||||
"background": {
|
||||
"removeSuccess": "Фон удалён.",
|
||||
"success": "Фон установлен.",
|
||||
"upload": "Выбрать фон со своего ПК",
|
||||
"remove": "Удалить фон",
|
||||
"title": "Установить фон списка",
|
||||
"searchPlaceholder": "Введи запрос для поиска фона…",
|
||||
"loadMore": "Загрузить больше фотографий"
|
||||
},
|
||||
"archive": {
|
||||
"title": "Заархивировать «{list}»",
|
||||
"unarchiveText": "Ты сможешь создавать новые задачи или изменять его.",
|
||||
"success": "Список архивирован.",
|
||||
"archiveText": "Ты не сможешь изменять этот список или создавать новые задачи, пока ты не вернёшь его из архива.",
|
||||
"unarchive": "Вернуть список из архива",
|
||||
"archive": "Архивировать этот список"
|
||||
},
|
||||
"create": {
|
||||
"createdSuccess": "Список создан.",
|
||||
"addTitleRequired": "Укажи название.",
|
||||
"titlePlaceholder": "Введи имя списка…",
|
||||
"header": "Создать новый список"
|
||||
},
|
||||
"lists": "Списки",
|
||||
"color": "Цвет",
|
||||
"title": "Название списка",
|
||||
"search": "Введи запрос для поиска списка…",
|
||||
"share": {
|
||||
"links": {
|
||||
"name": "Имя (необязательно)",
|
||||
"create": "Создать новую ссылку для обмена",
|
||||
"explanation": "Позволит тебе поделиться списком с теми, у кого нет аккаунта в Vikunja.",
|
||||
"what": "Что такое ссылка для обмена?",
|
||||
"title": "Ссылки для обмена",
|
||||
"noName": "Без имени",
|
||||
"deleteSuccess": "Ссылка удалена",
|
||||
"createSuccess": "Ссылка создана.",
|
||||
"removeText": "Удалить эту ссылку для обмена? Больше не удастся получить доступ к списку через эту ссылку. Это действие отменить нельзя!",
|
||||
"remove": "Удалить ссылку для обмена",
|
||||
"passwordExplanation": "Пользователь будет должен ввести пароль для входа.",
|
||||
"password": "Пароль (необязательно)",
|
||||
"nameExplanation": "Все действия, проведённые через эту ссылку, будут подписаны этим именем.",
|
||||
"namePlaceholder": "напр. Lorem Ipsum"
|
||||
},
|
||||
"share": "Поделиться",
|
||||
"title": "Поделиться списком «{list}»",
|
||||
"header": "Поделиться этим списком",
|
||||
"attributes": {
|
||||
"delete": "Удалить",
|
||||
"right": "Права",
|
||||
"sharedBy": "Создатель",
|
||||
"name": "Имя",
|
||||
"link": "Ссылка"
|
||||
},
|
||||
"right": {
|
||||
"admin": "Админ",
|
||||
"readWrite": "Чтение и запись",
|
||||
"read": "Только чтение",
|
||||
"title": "Права"
|
||||
}
|
||||
},
|
||||
"archived": "Этот список архивирован. В нём нелья создавать или изменять задачи."
|
||||
},
|
||||
"user": {
|
||||
"settings": {
|
||||
"totp": {
|
||||
"title": "Двухфакторная аутентификация",
|
||||
"disableSuccess": "Двухфакторная аутентификация отключена.",
|
||||
"confirmSuccess": "TOTP настроен и готов к использованию.",
|
||||
"disable": "Отключить двухфакторную аутентификацию",
|
||||
"enterPassword": "Введи свой пароль",
|
||||
"setupSuccess": "У тебя подключена двухфакторная аутентификация.",
|
||||
"passcodePlaceholder": "Код, который сгенерировал твой приложение TOTP",
|
||||
"passcode": "Код",
|
||||
"scanQR": "Или ты можешь отсканировать этот QR-код:",
|
||||
"finishSetupPart2": "Потом введи сюда код из приложения.",
|
||||
"finishSetupPart1": "Для завершения подключения используй этот секрет в своём приложении TOTP (andOTP и подобные):",
|
||||
"enroll": "Подключить"
|
||||
},
|
||||
"avatar": {
|
||||
"setSuccess": "Аватар установлен.",
|
||||
"statusUpdateSuccess": "Статус аватара обновлён.",
|
||||
"uploadAvatar": "Загрузить аватар",
|
||||
"initials": "Инициалы",
|
||||
"upload": "Загрузить файл",
|
||||
"gravatar": "Gravatar",
|
||||
"title": "Аватар"
|
||||
},
|
||||
"caldav": {
|
||||
"more": "Подробнее о caldav в Vikunja",
|
||||
"howTo": "Ты можешь подключить Vikunja к клиентам CalDAV, чтобы просматривать и управлять всеми задачами из разных клиентов. Введи этот URL в свой клиент:",
|
||||
"title": "CalDAV"
|
||||
},
|
||||
"general": {
|
||||
"language": "Язык",
|
||||
"weekStartMonday": "Понедельник",
|
||||
"weekStartSunday": "Воскресенье",
|
||||
"weekStart": "Первый день недели",
|
||||
"discoverableByEmail": "Разрешить другим пользователям находить меня по полному e-mail",
|
||||
"discoverableByName": "Разрешить другим находить меня по имени",
|
||||
"overdueReminders": "Присылать мне напоминания о просроченных невыполненных задачах на e-mail каждое утро",
|
||||
"emailReminders": "Присылать мне напоминания о задачах на e-mail",
|
||||
"savedSuccess": "Настройки обновлены.",
|
||||
"newName": "Новое имя",
|
||||
"name": "Имя",
|
||||
"title": "Основные настройки",
|
||||
"playSoundWhenDone": "Проигрывать звук, когда задача помечается завершённой"
|
||||
},
|
||||
"updateEmailSuccess": "E-mail успешно изменён. Для подтверждения нажми на ссылку в письме, которое мы тебе отправили.",
|
||||
"updateEmailNew": "Новый Email адрес",
|
||||
"updateEmailTitle": "Изменить E-mail",
|
||||
"passwordUpdateSuccess": "Пароль изменён.",
|
||||
"passwordsDontMatch": "Новые пароли не совпадают.",
|
||||
"currentPasswordPlaceholder": "Твой текущий пароль",
|
||||
"currentPassword": "Текущий пароль",
|
||||
"newPasswordConfirm": "Новый пароль ещё раз",
|
||||
"newPassword": "Новый пароль",
|
||||
"newPasswordTitle": "Изменить пароль",
|
||||
"title": "Настройки"
|
||||
},
|
||||
"auth": {
|
||||
"logout": "Выйти",
|
||||
"authenticating": "Аутентификация…",
|
||||
"loginWith": "Войти через {provider}",
|
||||
"register": "Зарегистрироваться",
|
||||
"login": "Войти",
|
||||
"totpTitle": "Код двухфакторной аутентификации",
|
||||
"confirmEmailSuccess": "Теперь ты можешь войти, используя свой e-mail.",
|
||||
"passwordsDontMatch": "Пароли не совпадают",
|
||||
"resetPasswordAction": "Отправить ссылку на сброс пароля",
|
||||
"resetPassword": "Сбросить пароль",
|
||||
"passwordRepeat": "Пароль ещё раз",
|
||||
"password": "Пароль",
|
||||
"email": "E-mail адрес",
|
||||
"usernameEmail": "Имя пользователя или Email",
|
||||
"username": "Имя пользователя",
|
||||
"resetPasswordSuccess": "Проверь почту! Там должно быть письмо с инструкциями, как сбросить пароль.",
|
||||
"passwordPlaceholder": "напр. •••••••••••",
|
||||
"totpPlaceholder": "напр. 123456",
|
||||
"emailPlaceholder": "напр. frederic@vikunja.io",
|
||||
"usernamePlaceholder": "напр. frederick"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"text": "Запрашиваемая страница не существует.",
|
||||
"title": "Не найдено"
|
||||
},
|
||||
"menu": {
|
||||
"newList": "Новый список",
|
||||
"share": "Поделиться",
|
||||
"setBackground": "Задать фон",
|
||||
"delete": "Удалить",
|
||||
"duplicate": "Создать копию",
|
||||
"edit": "Изменить",
|
||||
"unarchive": "Вернуть из архива",
|
||||
"archive": "Архивировать"
|
||||
},
|
||||
"update": {
|
||||
"do": "Обновить сейчас",
|
||||
"available": "Доступно обновление Vikunja!"
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"currentPageOnly": "Работают только на текущей странице.",
|
||||
"allPages": "Работают на всех страницах.",
|
||||
"title": "Сочетания клавиш",
|
||||
"task": {
|
||||
"assign": "Назначить задачу пользователю",
|
||||
"done": "Пометить задачу завершённой",
|
||||
"dueDate": "Изменить срок этой задачи",
|
||||
"related": "Изменить связанные задачи",
|
||||
"attachment": "Добавить вложение к задаче",
|
||||
"title": "Страница задачи",
|
||||
"labels": "Добавить метки этой задаче"
|
||||
},
|
||||
"toggleMenu": "Переключить меню",
|
||||
"quickSearch": "Открыть панель поиска/быстрых действий"
|
||||
},
|
||||
"team": {
|
||||
"attributes": {
|
||||
"member": "Участник",
|
||||
"admin": "Администратор",
|
||||
"descriptionPlaceholder": "Описание команды здесь…",
|
||||
"description": "Описание",
|
||||
"nameRequired": "Укажи имя.",
|
||||
"namePlaceholder": "Имя команды здесь…",
|
||||
"name": "Имя команды"
|
||||
},
|
||||
"edit": {
|
||||
"deleteUser": {
|
||||
"success": "Пользователь удалён из команды.",
|
||||
"text1": "Удалить этого пользователя из команды?",
|
||||
"header": "Удалить пользователя из команды",
|
||||
"text2": "Пользователь потеряет доступ ко всем спискам и пространствам имён, к котором есть доступ у команды. Это действие отменить нельзя!"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Команда удалена.",
|
||||
"text1": "Удалить эту команду вместе с участниками?",
|
||||
"header": "Удалить команду",
|
||||
"text2": "Все участники команды потеряют доступ к спискам и пространствам имён, которыми поделились с этой командой. Это действие отменить НЕЛЬЗЯ!"
|
||||
},
|
||||
"madeAdmin": "Участник команды теперь администратор.",
|
||||
"madeMember": "Участник команды теперь участник.",
|
||||
"userAddedSuccess": "Участник добавлен.",
|
||||
"success": "Команда обновлена.",
|
||||
"makeAdmin": "Сделать администратором",
|
||||
"makeMember": "Сделать участником",
|
||||
"addUser": "Добавить в команду",
|
||||
"members": "Участники команды",
|
||||
"title": "Изменить команду «{team}»",
|
||||
"search": "Введи запрос для поиска пользователя…"
|
||||
},
|
||||
"create": {
|
||||
"success": "Команда создана.",
|
||||
"title": "Создать команду"
|
||||
},
|
||||
"noTeams": "Ты сейчас не являешься участником ни одной команды.",
|
||||
"title": "Команды"
|
||||
},
|
||||
"task": {
|
||||
"repeat": {
|
||||
"years": "Лет",
|
||||
"months": "Месяцев",
|
||||
"weeks": "Недель",
|
||||
"days": "Дней",
|
||||
"each": "Каждые",
|
||||
"fromCurrentDate": "От сегодняшей даты",
|
||||
"monthly": "Ежемесячно",
|
||||
"mode": "Режим повтора",
|
||||
"everyMonth": "Каждый месяц",
|
||||
"everyWeek": "Каждую неделю",
|
||||
"everyDay": "Каждый день",
|
||||
"hours": "Часов"
|
||||
},
|
||||
"priority": {
|
||||
"high": "Высокий",
|
||||
"medium": "Средний",
|
||||
"low": "Низкий",
|
||||
"doNow": "СДЕЛАТЬ СЕЙЧАС",
|
||||
"urgent": "Срочный",
|
||||
"unset": "Не указан"
|
||||
},
|
||||
"label": {
|
||||
"removeSuccess": "Метка удалена.",
|
||||
"createSuccess": "Метка создана.",
|
||||
"addSuccess": "Метка добавлена.",
|
||||
"createPlaceholder": "Добавить как новую метку",
|
||||
"placeholder": "Введи новую метку…"
|
||||
},
|
||||
"description": {
|
||||
"empty": "Описания ещё нет.",
|
||||
"placeholder": "Нажми сюда для ввода описания…"
|
||||
},
|
||||
"deferDueDate": {
|
||||
"1week": "1 неделя",
|
||||
"3days": "3 дня",
|
||||
"1day": "1 день",
|
||||
"title": "Отложить срок"
|
||||
},
|
||||
"comment": {
|
||||
"addedSuccess": "Комментарий добавлен.",
|
||||
"deleteText2": "Это действие отменить нельзя!",
|
||||
"deleteText1": "Удалить этот комментарий?",
|
||||
"delete": "Удалить комментарий",
|
||||
"comment": "Комментировать",
|
||||
"placeholder": "Добавить комментарий…",
|
||||
"creating": "Комментируем…",
|
||||
"edited": "изменено {date}",
|
||||
"loading": "Загрузка комментариев…",
|
||||
"title": "Комментарии"
|
||||
},
|
||||
"attachment": {
|
||||
"deleteText2": "Это действие отменить нельзя!",
|
||||
"deleteText1": "Удалить вложение {filename}?",
|
||||
"delete": "Удалить вложение",
|
||||
"upload": "Загрузить вложение",
|
||||
"download": "Скачать",
|
||||
"createdBy": "создано {0} пользователем {1}",
|
||||
"title": "Вложения",
|
||||
"drop": "Перетащи файлы сюда для загрузки"
|
||||
},
|
||||
"subscription": {
|
||||
"unsubscribe": "Отписаться",
|
||||
"subscribe": "Подписаться",
|
||||
"unsubscribeSuccess": "Ты отписался от {entity}",
|
||||
"subscribeSuccess": "Ты подписался на {entity}",
|
||||
"notSubscribed": "Ты не подписан на {entity} и не будешь получать уведомления об изменениях.",
|
||||
"subscribed": "Ты подписан на {entity} и будешь получать уведомления об изменениях.",
|
||||
"subscribedThroughParent": "Ты не можешь отписаться здесь, потому что ты подписан на {entity} через {parent}."
|
||||
},
|
||||
"attributes": {
|
||||
"updated": "Дата изменения",
|
||||
"title": "Название",
|
||||
"repeat": "Повтор",
|
||||
"reminders": "Напоминания",
|
||||
"priority": "Приоритет",
|
||||
"labels": "Метки",
|
||||
"description": "Описание",
|
||||
"createdBy": "Создатель",
|
||||
"created": "Дата создания",
|
||||
"color": "Цвет",
|
||||
"assignees": "Пользователи, которым назначена задача",
|
||||
"percentDone": "% Завершено",
|
||||
"done": "Завершено",
|
||||
"startDate": "Дата начала",
|
||||
"endDate": "Дата завершения",
|
||||
"dueDate": "Срок",
|
||||
"relatedTasks": "Связанные задачи"
|
||||
},
|
||||
"detail": {
|
||||
"actions": {
|
||||
"delete": "Удалить задачу",
|
||||
"color": "Установить цвет задачи",
|
||||
"moveList": "Переместить задачу",
|
||||
"attachments": "Добавить вложения",
|
||||
"reminders": "Установить напоминания",
|
||||
"priority": "Установить приоритет",
|
||||
"label": "Добавить метки",
|
||||
"assign": "Назначить пользователю",
|
||||
"relatedTasks": "Добавить связанные задачи",
|
||||
"percentDone": "Установить процент завершения",
|
||||
"endDate": "Установить дату завершения",
|
||||
"startDate": "Установить дату начала",
|
||||
"dueDate": "Установить срок",
|
||||
"repeatAfter": "Установить интервал повтора"
|
||||
},
|
||||
"delete": {
|
||||
"text2": "Будут удалены все вложения, напоминания и отношения, связанные с этой задачей, и отменить это будет нельзя!",
|
||||
"text1": "Удалить эту задачу?",
|
||||
"header": "Удалить задачу"
|
||||
},
|
||||
"deleteSuccess": "Задача удалена.",
|
||||
"updateSuccess": "Задача сохранена.",
|
||||
"doneAt": "Завершено {0}",
|
||||
"updated": "Обновлено {0}",
|
||||
"created": "Создана {0} пользователем {1}",
|
||||
"undone": "Не завершено",
|
||||
"done": "Завершено!",
|
||||
"chooseEndDate": "Нажми для выбора даты завершения",
|
||||
"chooseStartDate": "Нажми для выбора даты начала",
|
||||
"chooseDueDate": "Нажми для выбора срока",
|
||||
"belongsToList": "Задача принадлежит списку «{list}»",
|
||||
"move": "Переместить задачу в другой список"
|
||||
},
|
||||
"show": {
|
||||
"noTasks": "Делать нечего. Хорошего дня!",
|
||||
"nextMonth": "Месяц",
|
||||
"nextWeek": "Неделя",
|
||||
"today": "Сегодня",
|
||||
"until": "по",
|
||||
"from": "Задачи с",
|
||||
"current": "Текущие задачи",
|
||||
"noDates": "Показать задачи без даты",
|
||||
"titleDates": "Задачи с {from} по {to}",
|
||||
"titleCurrent": "Текущие задачи"
|
||||
},
|
||||
"addReminder": "Добавить напоминание…",
|
||||
"createSuccess": "Задача удалена.",
|
||||
"delete": "Удалить задачу",
|
||||
"new": "Создать задачу",
|
||||
"task": "Задача",
|
||||
"assignee": {
|
||||
"unassignSuccess": "Пользователь убран.",
|
||||
"assignSuccess": "Пользователь назначен.",
|
||||
"selectPlaceholder": "Назначить этому пользователю",
|
||||
"placeholder": "Введи пользователя для назначения…"
|
||||
},
|
||||
"relation": {
|
||||
"deleteText2": "Это действие отменить нельзя!",
|
||||
"deleteText1": "Удалить эту связь с задачей?",
|
||||
"delete": "Удалить связь",
|
||||
"noneYet": "Ещё нет связанных задач.",
|
||||
"differentList": "Эта задача принадлежит другому списку.",
|
||||
"createPlaceholder": "Добавить как связанную задачу",
|
||||
"new": "Новая связанная задача",
|
||||
"add": "Добавить новую связанную задачу",
|
||||
"searchPlaceholder": "Введи запрос для поиска задачи, чтобы добавить связь…"
|
||||
},
|
||||
"undoneSuccess": "Задача отмечена как незавершённая.",
|
||||
"doneSuccess": "Задача отмечена как завершённая.",
|
||||
"openDetail": "Открыть подробный просмотр задачи"
|
||||
},
|
||||
"input": {
|
||||
"editor": {
|
||||
"guide": "Руководство",
|
||||
"horizontalRule": "Разделитель",
|
||||
"table": "Таблица",
|
||||
"image": "Изображение",
|
||||
"link": "Ссылка",
|
||||
"cleanBlock": "Очистить блок",
|
||||
"orderedList": "Нумерованный список",
|
||||
"unorderedList": "Маркированный список",
|
||||
"quote": "Цитата",
|
||||
"code": "Код",
|
||||
"strikethrough": "Зачёркнутый",
|
||||
"italic": "Курсив",
|
||||
"bold": "Жирный",
|
||||
"headingBigger": "Заголовок больше",
|
||||
"headingSmaller": "Заголовок меньше",
|
||||
"heading3": "Заголовок 3",
|
||||
"heading2": "Заголовок 2",
|
||||
"heading1": "Заголовок 1",
|
||||
"done": "Завершено"
|
||||
},
|
||||
"datepicker": {
|
||||
"nextWeek": "Через неделю",
|
||||
"nextMonday": "Следующий понедельник",
|
||||
"tomorrow": "Завтра",
|
||||
"today": "Сегодня",
|
||||
"chooseDate": "Выбрать дату",
|
||||
"thisWeekend": "Конец этой недели"
|
||||
},
|
||||
"resetColor": "Сбросить цвет"
|
||||
},
|
||||
"misc": {
|
||||
"saved": "Сохранено!",
|
||||
"saving": "Сохранение…",
|
||||
"doit": "Сделать это!",
|
||||
"create": "Создать",
|
||||
"next": "Вперёд",
|
||||
"previous": "Назад",
|
||||
"copy": "Скопировать в буфер обмена",
|
||||
"disable": "Отключить",
|
||||
"cancel": "Отмена",
|
||||
"delete": "Удалить",
|
||||
"save": "Сохранить",
|
||||
"loading": "Загрузка…",
|
||||
"default": "По умолчанию",
|
||||
"searchPlaceholder": "Введи запрос для поиска…",
|
||||
"search": "Поиск",
|
||||
"info": "Информация",
|
||||
"confirm": "Подтвердить",
|
||||
"refresh": "Обновить"
|
||||
},
|
||||
"navigation": {
|
||||
"settings": "Настройки",
|
||||
"upcoming": "Предстоящие задачи",
|
||||
"overview": "Обзор",
|
||||
"privacy": "Политика конфиденциальности"
|
||||
},
|
||||
"quickActions": {
|
||||
"cmds": {
|
||||
"newTeam": "Новая команда",
|
||||
"newList": "Новый список",
|
||||
"newTask": "Новая задача",
|
||||
"newNamespace": "Новое пространство имён"
|
||||
},
|
||||
"createTask": "Создать задачу в текущем списке ({title})",
|
||||
"newTeam": "Введи название новой команды…",
|
||||
"newTask": "Введи название задачи…",
|
||||
"newList": "Введи название списка…",
|
||||
"teams": "Команды",
|
||||
"lists": "Списки",
|
||||
"tasks": "Задачи",
|
||||
"hint": "Используй # для поиска только задач, * для поиска только списков и @ для поиска только команд.",
|
||||
"placeholder": "Введи команду или поисковый запрос…",
|
||||
"commands": "Команды",
|
||||
"createList": "Создать список в текущем пространстве имён ({title})",
|
||||
"newNamespace": "Введи название пространства имён…"
|
||||
},
|
||||
"notification": {
|
||||
"none": "Уведомлений нет. Хорошего дня!"
|
||||
},
|
||||
"loadingError": {
|
||||
"contact": "связаться с нами",
|
||||
"tryAgain": "попробуй ещё раз"
|
||||
},
|
||||
"apiConfig": {
|
||||
"signInOn": "Войди в свой аккаунт Vikunja на {0}",
|
||||
"change": "изменить",
|
||||
"url": "Vikunja URL",
|
||||
"urlPlaceholder": "напр. https://localhost:3456"
|
||||
},
|
||||
"error": {
|
||||
"6001": "Имя команды не может быть пустым.",
|
||||
"6002": "Команда не существует.",
|
||||
"4016": "Неверное поле задачи.",
|
||||
"4015": "Комментарий не существует.",
|
||||
"4013": "Неверный параметр сортировки.",
|
||||
"4012": "Вложение слишком большое.",
|
||||
"4011": "Вложение не существует.",
|
||||
"4010": "Нельзя связать задачу с собой.",
|
||||
"3007": "Список с таким идентификатором уже существует.",
|
||||
"3005": "Ты должен ввести название списка.",
|
||||
"3001": "Список не существует.",
|
||||
"2002": "Некоторые данные запроса неверны.",
|
||||
"2001": "ID не может быть пустой или 0.",
|
||||
"1018": "Тип аватара пользователя неверный.",
|
||||
"1014": "Старый пароль пустой.",
|
||||
"1013": "Новый пароль пустой.",
|
||||
"1012": "E-mail пользователя не подтверждён.",
|
||||
"1011": "Неверное имя пользователя или пароль.",
|
||||
"1010": "Неверный токен подтверждения e-mail.",
|
||||
"1009": "Неверный токен сброса пароля.",
|
||||
"1008": "Токен сброса пароля не предоставлен.",
|
||||
"1006": "Не удалось получить ID пользователя.",
|
||||
"1005": "Пользователь не существует.",
|
||||
"1004": "Не указаны имя пользователя и пароль.",
|
||||
"1002": "Пользователь с таким e-mail уже существует.",
|
||||
"1001": "Пользователь с таким именем уже существует.",
|
||||
"0001": "Тебе нельзя делать это.",
|
||||
"success": "Успех",
|
||||
"error": "Ошибка",
|
||||
"4009": "Эта связь с задачей не существует.",
|
||||
"4008": "Нельзя создать связь, которая уже существует.",
|
||||
"13002": "Неверный пароль ссылки для обмена.",
|
||||
"12001": "Некорректный тип подписки.",
|
||||
"9001": "Некорректные права.",
|
||||
"8003": "У тебя нет доступа к этой метке.",
|
||||
"8002": "Эта метка не существует.",
|
||||
"8001": "Эта метка уже существует в этой задаче.",
|
||||
"7003": "У тебя нет доступа к этому списку.",
|
||||
"7002": "У пользователя уже есть доступ к этому списку.",
|
||||
"6007": "У команды нет доступа к списку, чтобы выполнить это действие.",
|
||||
"6006": "Нельзя удалить последнего участника команды.",
|
||||
"6005": "Пользователь уже является участником этой команды.",
|
||||
"3004": "У тебя должны быть права на чтение этого списка, чтобы выполнить это действие.",
|
||||
"1017": "Неверный TOTP-код.",
|
||||
"1016": "Этот пользователь использует TOTP.",
|
||||
"1015": "Этот пользователь уже использует TOTP.",
|
||||
"10004": "Ты не можешь добавить задачу в эту колонку, так как достигнут лимит на число задач в ней.",
|
||||
"10003": "Ты не можешь удалить последнюю колонку списка.",
|
||||
"10002": "Эта колонка не принадлежит этому списку.",
|
||||
"10001": "Эта колонка не существует.",
|
||||
"6004": "Эта команда уже имеет доступ к этому пространству имён или списку.",
|
||||
"5011": "Этот пользователь уже имеет доступ к этому пространству имён.",
|
||||
"5010": "У этой команды нет доступа к этому пространству имён.",
|
||||
"5009": "Для этого действия необходим доступ на чтение пространства имён.",
|
||||
"5006": "Название пространства имён не может быть пустым.",
|
||||
"5003": "Нет доступа к указанному пространству имён.",
|
||||
"5001": "Пространство имён не существует.",
|
||||
"5012": "Это пространство имён архивировано и поэтому доступно только для чтения.",
|
||||
"3008": "Этот список архивирован и поэтому доступен только для чтения. Это также касается всех задач в этом списке."
|
||||
},
|
||||
"sharing": {
|
||||
"authenticating": "Аутентификация…",
|
||||
"invalidPassword": "Неверный пароль.",
|
||||
"error": "Случилась ошибка.",
|
||||
"passwordRequired": "Для этого общего списка нужен пароль. Введи его сюда:"
|
||||
},
|
||||
"migrate": {
|
||||
"inProgress": "Импортируем…",
|
||||
"getStarted": "Начать",
|
||||
"authorize": "Позволь VIkunja получить доступ к твоему аккаунту {name}, нажав кнопку ниже.",
|
||||
"descriptionDo": "Vikunja импортирует все списки, задачи, заметки, напоминания и файлы, к которым у тебя есть доступ.",
|
||||
"description": "Нажмите на логотип одного из сторонних сервисов, чтобы начать.",
|
||||
"import": "Импорт данных в Vikunja",
|
||||
"titleService": "Импорт твоих данных из {name} в Vikunja",
|
||||
"title": "Миграция из других сервисов в Vikunja",
|
||||
"alreadyMigrated1": "Кажется, ты уже импортировал данные из {name} {date}.",
|
||||
"confirm": "Я уверен, давай начнём миграцию!"
|
||||
},
|
||||
"namespace": {
|
||||
"attributes": {
|
||||
"color": "Цвет",
|
||||
"description": "Описание",
|
||||
"titlePlaceholder": "Введи название пространства имён…",
|
||||
"title": "Название пространства имён",
|
||||
"isArchived": "Это пространство имён архивировано",
|
||||
"archived": "Архивировано",
|
||||
"descriptionPlaceholder": "Введи описание пространства имён…"
|
||||
},
|
||||
"share": {
|
||||
"title": "Поделиться пространством имён «{namespace}»"
|
||||
},
|
||||
"edit": {
|
||||
"success": "Пространство имён обновлено.",
|
||||
"title": "Изменить «{namespace}»"
|
||||
},
|
||||
"delete": {
|
||||
"success": "Пространство имён удалено.",
|
||||
"text2": "Это включает в себя все списки и задачи, и отменить это будет нельзя!",
|
||||
"text1": "Удалить это пространство имён вместе со всем содержимым?",
|
||||
"title": "Удалить «{namespace}»"
|
||||
},
|
||||
"create": {
|
||||
"success": "Пространство имён создано.",
|
||||
"explanation": "Коллекции списков для совместного использования и организации ваших списков. Фактически, каждый список принадлежит какому-нибудь пространству имён.",
|
||||
"tooltip": "Что такое пространство имён?",
|
||||
"titleRequired": "Укажи название.",
|
||||
"title": "Создать новое пространство имён"
|
||||
},
|
||||
"search": "Введи запрос для поиска пространства имён…",
|
||||
"noLists": "В этом пространстве имён нет ни одного списка.",
|
||||
"namespaces": "Пространства имён",
|
||||
"createList": "Создать новый список в этом пространстве имён.",
|
||||
"noneAvailable": "Пространств имён сейчас нет.",
|
||||
"namespace": "Пространство имён",
|
||||
"title": "Пространства имён и списки",
|
||||
"archive": {
|
||||
"description": "Архивирование пространства имён означает, что ты не сможешь создавать в нём новые списки или изменять их.",
|
||||
"success": "Пространство имён архивировано.",
|
||||
"unarchiveText": "Ты сможешь создавать новые списки или изменять их.",
|
||||
"archiveText": "Ты не сможешь изменять это пространство имён, пока не вернёшь его из архива. Это также касается всех списков в этом пространстве имён.",
|
||||
"titleUnarchive": "Вернуть «{namespace}» из архива",
|
||||
"titleArchive": "Архивировать «{namespace}»"
|
||||
},
|
||||
"archived": "Архивировано",
|
||||
"unarchive": "Вернуть из архива",
|
||||
"showArchived": "Показать архив"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,8 +85,6 @@ import vueShortkey from 'vue-shortkey'
|
|||
import message from './message'
|
||||
import {colorIsDark} from './helpers/color/colorIsDark'
|
||||
import {setTitle} from './helpers/setTitle'
|
||||
import {getNamespaceTitle} from './helpers/getNamespaceTitle'
|
||||
import {getListTitle} from './helpers/getListTitle'
|
||||
// Vuex
|
||||
import {store} from './store'
|
||||
// i18n
|
||||
|
@ -201,12 +199,6 @@ Vue.mixin({
|
|||
formatDateShort(date) {
|
||||
return formatDate(date, 'PPpp', this.$t('date.locale'))
|
||||
},
|
||||
getNamespaceTitle(n) {
|
||||
return getNamespaceTitle(n, p => this.$t(p))
|
||||
},
|
||||
getListTitle(l) {
|
||||
return getListTitle(l, p => this.$t(p))
|
||||
},
|
||||
error(e, actions = []) {
|
||||
return message.error(e, this, p => this.$t(p), actions)
|
||||
},
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
export const getHistory = () => {
|
||||
const savedHistory = localStorage.getItem('listHistory')
|
||||
if (savedHistory === null) {
|
||||
return []
|
||||
}
|
||||
|
||||
return JSON.parse(savedHistory)
|
||||
}
|
||||
|
||||
export function saveListToHistory(list) {
|
||||
const history = getHistory()
|
||||
|
||||
// Remove the element if it already exists in history, preventing duplicates and essentially moving it to the beginning
|
||||
for (const i in history) {
|
||||
if (history[i].id === list.id) {
|
||||
history.splice(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
history.unshift(list)
|
||||
|
||||
if (history.length > 5) {
|
||||
history.pop()
|
||||
}
|
||||
localStorage.setItem('listHistory', JSON.stringify(history))
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
import {getHistory, saveListToHistory} from './listHistory'
|
||||
|
||||
test('return an empty history when none was saved', () => {
|
||||
Storage.prototype.getItem = jest.fn(() => null)
|
||||
const h = getHistory()
|
||||
expect(h).toStrictEqual([])
|
||||
})
|
||||
|
||||
test('return a saved history', () => {
|
||||
const saved = [{id: 1}, {id: 2}]
|
||||
Storage.prototype.getItem = jest.fn(() => JSON.stringify(saved))
|
||||
|
||||
const h = getHistory()
|
||||
expect(h).toStrictEqual(saved)
|
||||
})
|
||||
|
||||
test('store list in history', () => {
|
||||
let saved = {}
|
||||
Storage.prototype.getItem = jest.fn(() => null)
|
||||
Storage.prototype.setItem = jest.fn((key, lists) => {
|
||||
saved = lists
|
||||
})
|
||||
|
||||
saveListToHistory({id: 1})
|
||||
expect(saved).toBe('[{"id":1}]')
|
||||
})
|
||||
|
||||
test('store only the last 5 lists in history', () => {
|
||||
let saved = null
|
||||
Storage.prototype.getItem = jest.fn(() => saved)
|
||||
Storage.prototype.setItem = jest.fn((key, lists) => {
|
||||
saved = lists
|
||||
})
|
||||
|
||||
saveListToHistory({id: 1})
|
||||
saveListToHistory({id: 2})
|
||||
saveListToHistory({id: 3})
|
||||
saveListToHistory({id: 4})
|
||||
saveListToHistory({id: 5})
|
||||
saveListToHistory({id: 6})
|
||||
expect(saved).toBe('[{"id":6},{"id":5},{"id":4},{"id":3},{"id":2}]')
|
||||
})
|
||||
|
||||
test('don\'t store the same list twice', () => {
|
||||
let saved = null
|
||||
Storage.prototype.getItem = jest.fn(() => saved)
|
||||
Storage.prototype.setItem = jest.fn((key, lists) => {
|
||||
saved = lists
|
||||
})
|
||||
|
||||
saveListToHistory({id: 1})
|
||||
saveListToHistory({id: 1})
|
||||
expect(saved).toBe('[{"id":1}]')
|
||||
})
|
||||
|
||||
test('move a list to the beginning when storing it multiple times', () => {
|
||||
let saved = null
|
||||
Storage.prototype.getItem = jest.fn(() => saved)
|
||||
Storage.prototype.setItem = jest.fn((key, lists) => {
|
||||
saved = lists
|
||||
})
|
||||
|
||||
saveListToHistory({id: 1})
|
||||
saveListToHistory({id: 2})
|
||||
saveListToHistory({id: 1})
|
||||
expect(saved).toBe('[{"id":1},{"id":2}]')
|
||||
})
|
|
@ -5,7 +5,6 @@ import HomeComponent from '../views/Home'
|
|||
import NotFoundComponent from '../views/404'
|
||||
import LoadingComponent from '../components/misc/loading'
|
||||
import ErrorComponent from '../components/misc/error'
|
||||
import About from '../views/About'
|
||||
// User Handling
|
||||
import LoginComponent from '../views/user/Login'
|
||||
import RegisterComponent from '../views/user/Register'
|
||||
|
@ -528,10 +527,5 @@ export default new Router({
|
|||
name: 'openid.auth',
|
||||
component: OpenIdAuth,
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
component: About,
|
||||
},
|
||||
],
|
||||
})
|
|
@ -20,6 +20,7 @@ import attachments from './modules/attachments'
|
|||
import labels from './modules/labels'
|
||||
|
||||
import ListService from '../services/list'
|
||||
import {setTitle} from '@/helpers/setTitle'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
|
@ -68,6 +69,8 @@ export const store = new Vuex.Store({
|
|||
return
|
||||
}
|
||||
|
||||
setTitle(currentList.title)
|
||||
|
||||
// Not sure if this is the right way to do it but hey, it works
|
||||
if (
|
||||
// List changed
|
||||
|
@ -137,4 +140,4 @@ export const store = new Vuex.Store({
|
|||
state.quickActionsActive = active
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
|
@ -25,12 +25,6 @@ export default {
|
|||
}
|
||||
return null
|
||||
},
|
||||
findListByExactname: state => name => {
|
||||
const list = Object.values(state).find(l => {
|
||||
return l.title.toLowerCase() === name.toLowerCase()
|
||||
})
|
||||
return typeof list === 'undefined' ? null : list
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
toggleListFavorite(ctx, list) {
|
||||
|
@ -78,6 +72,6 @@ export default {
|
|||
return Promise.reject(e)
|
||||
})
|
||||
.finally(() => cancel())
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
|
@ -24,4 +24,3 @@
|
|||
@import 'datepicker';
|
||||
@import 'notifications';
|
||||
@import 'quick-actions';
|
||||
@import 'hint-modal';
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
.hint-modal {
|
||||
z-index: 4600;
|
||||
|
||||
.card-content {
|
||||
text-align: left;
|
||||
|
||||
.info {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.shortcuts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
padding: 0 .25rem;
|
||||
}
|
||||
|
||||
span {
|
||||
padding: .1rem .35rem;
|
||||
border: 1px solid $grey-300;
|
||||
background: $grey-100;
|
||||
border-radius: 3px;
|
||||
font-size: .75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-body {
|
||||
padding: .5rem .75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-container-smaller .hint-modal .modal-container {
|
||||
height: calc(100vh - 5rem);
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@ $bucket-background: $grey-100;
|
|||
$task-background: $white;
|
||||
$ease-out: all .3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
$bucket-width: 300px;
|
||||
$bucket-header-height: 60px;
|
||||
$bucket-right-margin: 1rem;
|
||||
|
||||
$crazy-height-calculation: '100vh - 4.5rem - 1.5rem - 1rem - 1.5rem - 11px';
|
||||
$crazy-height-calculation-tasks: '#{$crazy-height-calculation} - 1rem - 2.5rem - 2rem - #{$button-height} - 1rem';
|
||||
|
@ -33,7 +31,7 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
|||
position: relative;
|
||||
|
||||
flex: 0 0 $bucket-width;
|
||||
margin: 0 $bucket-right-margin 0 0;
|
||||
margin: 0 1rem 0 0;
|
||||
max-height: 100%;
|
||||
min-height: 20px;
|
||||
max-width: $bucket-width;
|
||||
|
@ -235,18 +233,6 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
|||
a.dropdown-item {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
&.is-collapsed {
|
||||
transform: rotate(90deg) translateX($bucket-width / 2 - $bucket-header-height / 2);
|
||||
// Using negative margins instead of translateY here to make all other buckets fill the empty space
|
||||
margin-left: ($bucket-width / 2 - $bucket-header-height / 2) * -1;
|
||||
margin-right: calc(#{($bucket-width / 2 - $bucket-header-height / 2) * -1} + #{$bucket-right-margin});
|
||||
cursor: pointer;
|
||||
|
||||
.tasks, .bucket-footer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bucket-header {
|
||||
|
@ -254,7 +240,6 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: .5rem;
|
||||
height: $bucket-header-height;
|
||||
|
||||
.limit {
|
||||
padding-left: .5rem;
|
||||
|
|
|
@ -7,3 +7,42 @@
|
|||
color: $grey-500;
|
||||
transition: color $transition;
|
||||
}
|
||||
|
||||
.keyboard-shortcuts-modal {
|
||||
z-index: 4600;
|
||||
|
||||
.card-content {
|
||||
text-align: left;
|
||||
|
||||
.info {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.shortcuts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
padding: 0 .25rem;
|
||||
}
|
||||
|
||||
span {
|
||||
padding: .1rem .35rem;
|
||||
border: 1px solid $grey-300;
|
||||
background: $grey-100;
|
||||
border-radius: 3px;
|
||||
font-size: .75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-body {
|
||||
padding: .5rem .75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -163,140 +163,3 @@ $filter-container-top-link-share-list: -47px;
|
|||
.is-archived .notification.is-warning {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
$lists-per-row: 5;
|
||||
$list-height: 150px;
|
||||
$list-spacing: 1rem;
|
||||
|
||||
.list-card {
|
||||
cursor: pointer;
|
||||
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
|
||||
height: $list-height;
|
||||
background: $white;
|
||||
margin: 0 $list-spacing $list-spacing 0;
|
||||
padding: 1rem;
|
||||
border-radius: $radius;
|
||||
box-shadow: $shadow-sm;
|
||||
transition: box-shadow $transition;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $shadow-md;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:focus:not(:active) {
|
||||
box-shadow: $shadow-xs !important;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $widescreen) {
|
||||
&:nth-child(#{$lists-per-row}n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $widescreen) and (min-width: $tablet) {
|
||||
$lists-per-row: 3;
|
||||
& {
|
||||
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
|
||||
}
|
||||
|
||||
&:nth-child(#{$lists-per-row}n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
$lists-per-row: 2;
|
||||
& {
|
||||
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
|
||||
}
|
||||
|
||||
&:nth-child(#{$lists-per-row}n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $mobile) {
|
||||
$lists-per-row: 1;
|
||||
& {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.is-archived-container {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
|
||||
.is-archived {
|
||||
font-size: .75rem;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
align-self: flex-end;
|
||||
font-family: $vikunja-font;
|
||||
font-weight: 400;
|
||||
font-size: 1.5rem;
|
||||
color: $text;
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
max-height: calc(100% - 2rem); // 1rem padding, 1rem height of the "is archived" badge
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
&.has-light-text .title {
|
||||
color: $light;
|
||||
}
|
||||
|
||||
&.has-background {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
|
||||
.title {
|
||||
text-shadow: 0 0 10px $black, 1px 1px 5px $grey-700, -1px -1px 5px $grey-700;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.favorite {
|
||||
transition: opacity $transition, color $transition;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
color: $orange;
|
||||
}
|
||||
|
||||
&.is-archived {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.is-favorite {
|
||||
display: inline-block;
|
||||
opacity: 1;
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .favorite {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.list-cards-wrapper-2-rows {
|
||||
flex-wrap: wrap;
|
||||
max-height: calc(#{$list-height * 2} + #{$list-spacing * 2});
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
$lists-per-row: 5;
|
||||
|
||||
.namespaces-list {
|
||||
.button.new-namespace {
|
||||
float: right;
|
||||
|
@ -42,6 +44,133 @@
|
|||
.lists {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
||||
.list {
|
||||
cursor: pointer;
|
||||
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
|
||||
height: 150px;
|
||||
background: $white;
|
||||
margin: 0 1rem 1rem 0;
|
||||
padding: 1rem;
|
||||
border-radius: $radius;
|
||||
box-shadow: $shadow-sm;
|
||||
transition: box-shadow $transition;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $shadow-md;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:focus:not(:active) {
|
||||
box-shadow: $shadow-xs !important;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $widescreen) {
|
||||
&:nth-child(#{$lists-per-row}n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $widescreen) and (min-width: $tablet) {
|
||||
$lists-per-row: 3;
|
||||
& {
|
||||
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
|
||||
}
|
||||
|
||||
&:nth-child(#{$lists-per-row}n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
$lists-per-row: 2;
|
||||
& {
|
||||
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
|
||||
}
|
||||
|
||||
&:nth-child(#{$lists-per-row}n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $mobile) {
|
||||
$lists-per-row: 1;
|
||||
& {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.is-archived-container {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
|
||||
.is-archived {
|
||||
font-size: .75rem;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
align-self: flex-end;
|
||||
font-family: $vikunja-font;
|
||||
font-weight: 400;
|
||||
font-size: 1.5rem;
|
||||
color: $text;
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
max-height: calc(100% - 2rem); // 1rem padding, 1rem height of the "is archived" badge
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
&.has-light-text .title {
|
||||
color: $light;
|
||||
}
|
||||
|
||||
&.has-background {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
|
||||
.title {
|
||||
text-shadow: 0 0 10px $black, 1px 1px 5px $grey-700, -1px -1px 5px $grey-700;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.favorite {
|
||||
transition: opacity $transition, color $transition;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
color: $orange;
|
||||
}
|
||||
|
||||
&.is-archived {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.is-favorite {
|
||||
display: inline-block;
|
||||
opacity: 1;
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .favorite {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -144,7 +144,3 @@ button.table {
|
|||
box-shadow: $shadow-md;
|
||||
}
|
||||
}
|
||||
|
||||
.is-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
<template>
|
||||
<transition name="fade">
|
||||
<div class="modal-mask hint-modal">
|
||||
<div @click.self="$router.back()" class="modal-container">
|
||||
<div class="modal-content">
|
||||
<card
|
||||
class="has-background-white has-no-shadow"
|
||||
:title="$t('about.title')"
|
||||
:has-close="true"
|
||||
close-icon="times"
|
||||
@close="$router.back()"
|
||||
:padding="false"
|
||||
>
|
||||
<div class="p-4">
|
||||
<p>
|
||||
{{ $t('about.frontendVersion', {version: this.frontendVersion}) }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('about.apiVersion', {version: this.apiVersion}) }}
|
||||
</p>
|
||||
</div>
|
||||
<footer class="modal-card-foot is-flex is-justify-content-flex-end">
|
||||
<x-button
|
||||
type="secondary"
|
||||
@click.prevent.stop="$router.back()"
|
||||
>
|
||||
{{ $t('misc.close') }}
|
||||
</x-button>
|
||||
</footer>
|
||||
</card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {VERSION} from '../version.json'
|
||||
|
||||
export default {
|
||||
name: 'About',
|
||||
computed: {
|
||||
frontendVersion() {
|
||||
return VERSION
|
||||
},
|
||||
apiVersion() {
|
||||
return this.$store.state.config.version
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="content has-text-centered">
|
||||
<h2>
|
||||
{{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
|
||||
{{ $t('home.welcome', {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
|
||||
</h2>
|
||||
<template v-if="!hasTasks">
|
||||
<p>{{ $t('home.list.newText') }}</p>
|
||||
|
@ -23,17 +23,6 @@
|
|||
{{ $t('home.list.import') }}
|
||||
</x-button>
|
||||
</template>
|
||||
<div v-if="listHistory.length > 0" class="is-max-width-desktop has-text-left">
|
||||
<h3>{{ $t('home.lastViewed') }}</h3>
|
||||
<div class="is-flex list-cards-wrapper-2-rows">
|
||||
<list-card
|
||||
v-for="(l, k) in listHistory"
|
||||
:key="`l${k}`"
|
||||
:list="l"
|
||||
:background-resolver="() => null"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ShowTasks :show-all="true" v-if="hasLists"/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -41,13 +30,10 @@
|
|||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import ShowTasks from './tasks/ShowTasks'
|
||||
import {getHistory} from '@/modules/listHistory'
|
||||
import ListCard from '@/components/list/partials/list-card'
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
ListCard,
|
||||
ShowTasks,
|
||||
},
|
||||
data() {
|
||||
|
@ -57,54 +43,25 @@ export default {
|
|||
tasks: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
welcome() {
|
||||
const now = new Date()
|
||||
|
||||
if (now.getHours() < 5) {
|
||||
return 'Night'
|
||||
computed: mapState({
|
||||
migratorsEnabled: state => state.config.availableMigrators !== null && state.config.availableMigrators.length > 0,
|
||||
authenticated: state => state.auth.authenticated,
|
||||
userInfo: state => state.auth.info,
|
||||
hasTasks: state => state.hasTasks,
|
||||
defaultNamespaceId: state => {
|
||||
if (state.namespaces.namespaces.length === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (now.getHours() < 11) {
|
||||
return 'Morning'
|
||||
}
|
||||
|
||||
if (now.getHours() < 18) {
|
||||
return 'Day'
|
||||
}
|
||||
|
||||
if (now.getHours() < 23) {
|
||||
return 'Evening'
|
||||
}
|
||||
|
||||
return 'Night'
|
||||
return state.namespaces.namespaces[0].id
|
||||
},
|
||||
listHistory() {
|
||||
const history = getHistory()
|
||||
return history.map(l => {
|
||||
return this.$store.getters['lists/getListById'](l.id)
|
||||
})
|
||||
hasLists: state => {
|
||||
if (state.namespaces.namespaces.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return state.namespaces.namespaces[0].lists.length > 0
|
||||
},
|
||||
...mapState({
|
||||
migratorsEnabled: state => state.config.availableMigrators !== null && state.config.availableMigrators.length > 0,
|
||||
authenticated: state => state.auth.authenticated,
|
||||
userInfo: state => state.auth.info,
|
||||
hasTasks: state => state.hasTasks,
|
||||
defaultNamespaceId: state => {
|
||||
if (state.namespaces.namespaces.length === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return state.namespaces.namespaces[0].id
|
||||
},
|
||||
hasLists: state => {
|
||||
if (state.namespaces.namespaces.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return state.namespaces.namespaces[0].lists.length > 0
|
||||
},
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="modal-mask hint-modal">
|
||||
<div class="modal-mask keyboard-shortcuts-modal">
|
||||
<div @click.self="$router.back()" class="modal-container">
|
||||
<div class="modal-content">
|
||||
<card class="has-background-white has-no-shadow" :title="$t('filters.create.title')">
|
||||
|
|
|
@ -44,7 +44,6 @@ import ListModel from '../../models/list'
|
|||
import ListService from '../../services/list'
|
||||
import {CURRENT_LIST} from '@/store/mutation-types'
|
||||
import {getListView} from '@/helpers/saveListView'
|
||||
import {saveListToHistory} from '@/modules/listHistory'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
@ -93,11 +92,7 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
const listData = {id: this.$route.params.listId}
|
||||
|
||||
saveListToHistory(listData)
|
||||
|
||||
this.setTitle(this.currentList.id ? this.getListTitle(this.currentList) : '')
|
||||
this.setTitle(this.currentList.title)
|
||||
|
||||
// This invalidates the loaded list at the kanban board which lets it reload its content when
|
||||
// switched to it. This ensures updates done to tasks in the gantt or list views are consistently
|
||||
|
@ -139,12 +134,11 @@ export default {
|
|||
console.debug(`Loading list, $route.name = ${this.$route.name}, $route.params =`, this.$route.params, `, listLoaded = ${this.listLoaded}, currentList = `, this.currentList)
|
||||
|
||||
// We create an extra list object instead of creating it in this.list because that would trigger a ui update which would result in bad ux.
|
||||
const list = new ListModel(listData)
|
||||
let list = new ListModel({id: this.$route.params.listId})
|
||||
this.listService.get(list)
|
||||
.then(r => {
|
||||
this.$set(this, 'list', r)
|
||||
this.$store.commit(CURRENT_LIST, r)
|
||||
this.setTitle(this.getListTitle(r))
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e)
|
||||
|
@ -155,4 +149,4 @@ export default {
|
|||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</script>
|
|
@ -17,13 +17,8 @@
|
|||
/>
|
||||
</div>
|
||||
<div :class="{ 'is-loading': loading && !oneTaskUpdating}" class="kanban loader-container">
|
||||
<div
|
||||
:key="`bucket${bucket.id}`"
|
||||
class="bucket"
|
||||
:class="{'is-collapsed': collapsedBuckets[bucket.id]}"
|
||||
v-for="bucket in buckets"
|
||||
>
|
||||
<div class="bucket-header" @click="() => unCollapseBucket(bucket)">
|
||||
<div :key="`bucket${bucket.id}`" class="bucket" v-for="bucket in buckets">
|
||||
<div class="bucket-header">
|
||||
<span
|
||||
v-if="bucket.isDoneBucket"
|
||||
class="icon is-small has-text-success mr-2"
|
||||
|
@ -36,7 +31,7 @@
|
|||
@focusout="() => saveBucketTitle(bucket.id)"
|
||||
@keydown.enter.prevent.stop="() => saveBucketTitle(bucket.id)"
|
||||
class="title input"
|
||||
:contenteditable="canWrite && !collapsedBuckets[bucket.id]"
|
||||
:contenteditable="canWrite"
|
||||
spellcheck="false">{{ bucket.title }}</h2>
|
||||
<span
|
||||
:class="{'is-max': bucket.tasks.length >= bucket.limit}"
|
||||
|
@ -46,7 +41,7 @@
|
|||
</span>
|
||||
<dropdown
|
||||
class="is-right options"
|
||||
v-if="canWrite && !collapsedBuckets[bucket.id]"
|
||||
v-if="canWrite"
|
||||
trigger-icon="ellipsis-v"
|
||||
@close="() => showSetLimitInput = false"
|
||||
>
|
||||
|
@ -76,13 +71,11 @@
|
|||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
{{
|
||||
$t('list.kanban.limit', {limit: bucket.limit > 0 ? bucket.limit : $t('list.kanban.noLimit')})
|
||||
}}
|
||||
{{ $t('list.kanban.limit', {limit: bucket.limit > 0 ? bucket.limit : $t('list.kanban.noLimit') }) }}
|
||||
</template>
|
||||
</a>
|
||||
<a
|
||||
@click.stop="toggleDoneBucket(bucket)"
|
||||
@click="toggleDoneBucket(bucket)"
|
||||
class="dropdown-item"
|
||||
v-tooltip="$t('list.kanban.doneBucketHintExtended')"
|
||||
>
|
||||
|
@ -90,15 +83,9 @@
|
|||
icon="check-double"/></span>
|
||||
{{ $t('list.kanban.doneBucket') }}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click.stop="() => collapseBucket(bucket)"
|
||||
>
|
||||
{{ $t('list.kanban.collapse') }}
|
||||
</a>
|
||||
<a
|
||||
:class="{'is-disabled': buckets.length <= 1}"
|
||||
@click.stop="() => deleteBucketModal(bucket.id)"
|
||||
@click="() => deleteBucketModal(bucket.id)"
|
||||
class="dropdown-item has-text-danger"
|
||||
v-tooltip="buckets.length <= 1 ? $t('list.kanban.deleteLast') : ''"
|
||||
>
|
||||
|
@ -277,6 +264,7 @@
|
|||
|
||||
<script>
|
||||
import TaskService from '../../../services/task'
|
||||
import TaskModel from '../../../models/task'
|
||||
import BucketModel from '../../../models/bucket'
|
||||
|
||||
import {Container, Draggable} from 'vue-smooth-dnd'
|
||||
|
@ -293,8 +281,6 @@ import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
|
|||
import FilterPopup from '@/components/list/partials/filter-popup'
|
||||
import Dropdown from '@/components/misc/dropdown'
|
||||
import {playPop} from '@/helpers/playPop'
|
||||
import createTask from '@/components/tasks/mixins/createTask'
|
||||
import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState'
|
||||
|
||||
export default {
|
||||
name: 'Kanban',
|
||||
|
@ -327,7 +313,6 @@ export default {
|
|||
showNewBucketInput: false,
|
||||
newTaskError: {},
|
||||
showSetLimitInput: false,
|
||||
collapsedBuckets: {},
|
||||
|
||||
// We're using this to show the loading animation only at the task when updating it
|
||||
taskUpdating: {},
|
||||
|
@ -343,9 +328,6 @@ export default {
|
|||
filtersChanged: false, // To trigger a reload of the board
|
||||
}
|
||||
},
|
||||
mixins: [
|
||||
createTask,
|
||||
],
|
||||
created() {
|
||||
this.taskService = new TaskService()
|
||||
this.loadBuckets()
|
||||
|
@ -382,8 +364,6 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
this.collapsedBuckets = getCollapsedBucketState(this.$route.params.listId)
|
||||
|
||||
console.debug(`Loading buckets, loadedListId = ${this.loadedListId}, $route.params =`, this.$route.params)
|
||||
this.filtersChanged = false
|
||||
|
||||
|
@ -508,7 +488,24 @@ export default {
|
|||
}
|
||||
this.$set(this.newTaskError, bucketId, false)
|
||||
|
||||
this.createNewTask(this.newTaskText, bucketId)
|
||||
// We need the actual bucket index so we put that in a seperate function
|
||||
const bucketIndex = () => {
|
||||
for (const t in this.buckets) {
|
||||
if (this.buckets[t].id === bucketId) {
|
||||
return t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bi = bucketIndex()
|
||||
|
||||
const task = new TaskModel({
|
||||
title: this.newTaskText,
|
||||
bucketId: this.buckets[bi].id,
|
||||
listId: this.$route.params.listId,
|
||||
})
|
||||
|
||||
this.taskService.create(task)
|
||||
.then(r => {
|
||||
this.newTaskText = ''
|
||||
this.$store.commit('kanban/addTaskToBucket', r)
|
||||
|
@ -517,10 +514,10 @@ export default {
|
|||
this.error(e)
|
||||
})
|
||||
.finally(() => {
|
||||
if (!this.$refs[`tasks-container${bucketId}`][0]) {
|
||||
if (!this.$refs[`tasks-container${task.bucketId}`][0]) {
|
||||
return
|
||||
}
|
||||
this.$refs[`tasks-container${bucketId}`][0].scrollTop = this.$refs[`tasks-container${bucketId}`][0].scrollHeight
|
||||
this.$refs[`tasks-container${task.bucketId}`][0].scrollTop = this.$refs[`tasks-container${task.bucketId}`][0].scrollHeight
|
||||
})
|
||||
},
|
||||
createNewBucket() {
|
||||
|
@ -627,18 +624,6 @@ export default {
|
|||
bucket.isDoneBucket = !bucket.isDoneBucket
|
||||
})
|
||||
},
|
||||
collapseBucket(bucket) {
|
||||
this.$set(this.collapsedBuckets, bucket.id, true)
|
||||
saveCollapsedBucketState(this.$route.params.listId, this.collapsedBuckets)
|
||||
},
|
||||
unCollapseBucket(bucket) {
|
||||
if (!this.collapsedBuckets[bucket.id]) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$set(this.collapsedBuckets, bucket.id, false)
|
||||
saveCollapsedBucketState(this.$route.params.listId, this.collapsedBuckets)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -82,7 +82,6 @@
|
|||
<p class="help is-danger" v-if="showError && newTaskText === ''">
|
||||
{{ $t('list.list.addTitleRequired') }}
|
||||
</p>
|
||||
<quick-add-magic v-if="!showError"/>
|
||||
</div>
|
||||
|
||||
<nothing v-if="ctaVisible && tasks.length === 0 && !taskCollectionService.loading">
|
||||
|
@ -110,8 +109,7 @@
|
|||
</div>
|
||||
<card
|
||||
v-if="isTaskEdit"
|
||||
class="taskedit mt-0" :title="$t('list.list.editTask')" :has-close="true"
|
||||
@close="() => isTaskEdit = false"
|
||||
class="taskedit mt-0" :title="$t('list.list.editTask')" :has-close="true" @close="() => isTaskEdit = false"
|
||||
:shadow="false">
|
||||
<edit-task :task="taskEditTask"/>
|
||||
</card>
|
||||
|
@ -158,13 +156,15 @@
|
|||
<router-view/>
|
||||
</transition>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TaskService from '../../../services/task'
|
||||
import TaskModel from '../../../models/task'
|
||||
import LabelTaskService from '../../../services/labelTask'
|
||||
import LabelTask from '../../../models/labelTask'
|
||||
import LabelModel from '../../../models/label'
|
||||
|
||||
import EditTask from '../../../components/tasks/edit-task'
|
||||
import SingleTaskInList from '../../../components/tasks/partials/singleTaskInList'
|
||||
|
@ -173,9 +173,8 @@ import {saveListView} from '@/helpers/saveListView'
|
|||
import Rights from '../../../models/rights.json'
|
||||
import {mapState} from 'vuex'
|
||||
import FilterPopup from '@/components/list/partials/filter-popup'
|
||||
import {HAS_TASKS} from '@/store/mutation-types'
|
||||
import Nothing from '@/components/misc/nothing'
|
||||
import createTask from '@/components/tasks/mixins/createTask'
|
||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic'
|
||||
|
||||
export default {
|
||||
name: 'List',
|
||||
|
@ -185,16 +184,17 @@ export default {
|
|||
isTaskEdit: false,
|
||||
taskEditTask: TaskModel,
|
||||
newTaskText: '',
|
||||
|
||||
showError: false,
|
||||
labelTaskService: LabelTaskService,
|
||||
|
||||
ctaVisible: false,
|
||||
}
|
||||
},
|
||||
mixins: [
|
||||
taskList,
|
||||
createTask,
|
||||
],
|
||||
components: {
|
||||
QuickAddMagic,
|
||||
Nothing,
|
||||
FilterPopup,
|
||||
SingleTaskInList,
|
||||
|
@ -202,6 +202,7 @@ export default {
|
|||
},
|
||||
created() {
|
||||
this.taskService = new TaskService()
|
||||
this.labelTaskService = new LabelTaskService()
|
||||
|
||||
// Save the current list view to local storage
|
||||
// We use local storage and not vuex here to make it persistent across reloads.
|
||||
|
@ -228,11 +229,148 @@ export default {
|
|||
}
|
||||
this.showError = false
|
||||
|
||||
this.createNewTask(this.newTaskText)
|
||||
let task = new TaskModel({title: this.newTaskText, listId: this.$route.params.listId})
|
||||
this.taskService.create(task)
|
||||
.then(task => {
|
||||
this.tasks.push(task)
|
||||
this.sortTasks()
|
||||
this.newTaskText = ''
|
||||
|
||||
// Unlike a proper programming language, Javascript only knows references to objects and does not
|
||||
// allow you to control what is a reference and what isnt. Because of this we can't just add
|
||||
// all labels to the task they belong to right after we found and added them to the task since
|
||||
// the task update method also ensures all data the api sees has the right format. That means
|
||||
// it processes labels. That processing changes the date format and the label color and makes
|
||||
// the label pretty much unusable for everything else. Normally, this is not a big deal, because
|
||||
// the labels on a task get thrown away anyway and replaced with the new models from the api
|
||||
// when we get the updated answer back. However, in this specific case because we're passing a
|
||||
// label we obtained from vuex that reference is kept and not thrown away. The task itself gets
|
||||
// a new label object - you won't notice the bad reference until you want to add the same label
|
||||
// again and notice it doesn't have a color anymore.
|
||||
// I think this is what happens: (or rather would happen without the hack I've put in)
|
||||
// 1. Query the store for a label which matches the name
|
||||
// 2. Find one - remember, we get only a *reference* to the label from the store, not a new label object.
|
||||
// (Now there's *two* places with a reference to the same label object: in the store and in the
|
||||
// variable which holds the label from the search in the store)
|
||||
// 3. .push the label to the task
|
||||
// 4. Update the task to remove the labels from the name
|
||||
// 4.1. The task update processes all labels belonging to that task, changing attributes of our
|
||||
// label in the process. Because this is a reference, it is also "updated" in the store.
|
||||
// 5. Get an api response back. The service handler now creates a new label object for all labels
|
||||
// returned from the api. It will throw away all references to the old label in the process.
|
||||
// 6. Now we have two objects with the same label data: The old one we originally obtained from
|
||||
// the store and the one that was created when parsing the api response. The old one was
|
||||
// modified before sending the api request and thus, our store which still holds a reference
|
||||
// to the old label now contains old data.
|
||||
// I guess this is the point where normally the GC would come in and collect the old label
|
||||
// object if the store wouldn't still hold a reference to it.
|
||||
//
|
||||
// Now, as a workaround, I'm putting all new labels added to that task in this separate variable to
|
||||
// add them only after the task was updated to circumvent the task update service processing the
|
||||
// label before sending it. Feels more hacky than it probably is.
|
||||
const newLabels = []
|
||||
|
||||
// Check if the task has words starting with ~ in the title and make them to labels
|
||||
const parts = task.title.split(' ~')
|
||||
// The first element will always contain the title, even if there is no occurrence of ~
|
||||
if (parts.length > 1) {
|
||||
|
||||
// First, create an unresolved promise for each entry in the array to wait
|
||||
// until all labels are added to update the task title once again
|
||||
let labelAddings = []
|
||||
let labelAddsToWaitFor = []
|
||||
parts.forEach((p, index) => {
|
||||
if (index < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
labelAddsToWaitFor.push(new Promise((resolve, reject) => {
|
||||
labelAddings.push({resolve: resolve, reject: reject})
|
||||
}))
|
||||
})
|
||||
|
||||
// Then do everything that is involved in finding, creating and adding the label to the task
|
||||
parts.forEach((p, index) => {
|
||||
if (index < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
// The part up until the next space
|
||||
const labelTitle = p.split(' ')[0]
|
||||
|
||||
// Don't create an empty label
|
||||
if (labelTitle === '') {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the label exists
|
||||
const label = Object.values(this.$store.state.labels.labels).find(l => {
|
||||
return l.title.toLowerCase() === labelTitle.toLowerCase()
|
||||
})
|
||||
|
||||
// Label found, use it
|
||||
if (typeof label !== 'undefined') {
|
||||
const labelTask = new LabelTask({
|
||||
taskId: task.id,
|
||||
labelId: label.id,
|
||||
})
|
||||
this.labelTaskService.create(labelTask)
|
||||
.then(result => {
|
||||
newLabels.push(label)
|
||||
|
||||
// Remove the label text from the task title
|
||||
task.title = task.title.replace(` ~${labelTitle}`, '')
|
||||
|
||||
// Make the promise done (the one with the index 0 does not exist)
|
||||
labelAddings[index - 1].resolve(result)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e)
|
||||
})
|
||||
} else {
|
||||
// label not found, create it
|
||||
const label = new LabelModel({title: labelTitle})
|
||||
this.$store.dispatch('labels/createLabel', label)
|
||||
.then(res => {
|
||||
const labelTask = new LabelTask({
|
||||
taskId: task.id,
|
||||
labelId: res.id,
|
||||
})
|
||||
this.labelTaskService.create(labelTask)
|
||||
.then(result => {
|
||||
newLabels.push(res)
|
||||
|
||||
// Remove the label text from the task title
|
||||
task.title = task.title.replace(` ~${labelTitle}`, '')
|
||||
|
||||
// Make the promise done (the one with the index 0 does not exist)
|
||||
labelAddings[index - 1].resolve(result)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e)
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// This waits to update the task until all labels have been added and the title has
|
||||
// been modified to remove each label text
|
||||
Promise.all(labelAddsToWaitFor)
|
||||
.then(() => {
|
||||
this.taskService.update(task)
|
||||
.then(updatedTask => {
|
||||
updatedTask.labels = newLabels
|
||||
this.updateTasks(updatedTask)
|
||||
this.$store.commit(HAS_TASKS, true)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e)
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
</x-button>
|
||||
|
||||
<h1>
|
||||
<span>{{ getNamespaceTitle(n) }}</span>
|
||||
<span>{{ n.title }}</span>
|
||||
<span class="is-archived" v-if="n.isArchived">
|
||||
{{ $t('namespace.archived') }}
|
||||
</span>
|
||||
|
@ -53,12 +53,37 @@
|
|||
</p>
|
||||
|
||||
<div class="lists">
|
||||
<list-card
|
||||
v-for="l in n.lists"
|
||||
:key="`l${l.id}`"
|
||||
:list="l"
|
||||
:show-archived="showArchived"
|
||||
/>
|
||||
<template v-for="l in n.lists">
|
||||
<router-link
|
||||
:class="{
|
||||
'has-light-text': !colorIsDark(l.hexColor),
|
||||
'has-background': typeof backgrounds[l.id] !== 'undefined',
|
||||
}"
|
||||
:key="`l${l.id}`"
|
||||
:style="{
|
||||
'background-color': l.hexColor,
|
||||
'background-image': typeof backgrounds[l.id] !== 'undefined' ? `url(${backgrounds[l.id]})` : false,
|
||||
}"
|
||||
:to="{ name: 'list.index', params: { listId: l.id} }"
|
||||
class="list"
|
||||
tag="span"
|
||||
v-if="showArchived ? true : !l.isArchived"
|
||||
>
|
||||
<div class="is-archived-container">
|
||||
<span class="is-archived" v-if="l.isArchived">
|
||||
{{ $t('namespace.archived') }}
|
||||
</span>
|
||||
<span
|
||||
:class="{'is-favorite': l.isFavorite, 'is-archived': l.isArchived}"
|
||||
@click.stop="toggleFavoriteList(l)"
|
||||
class="favorite">
|
||||
<icon icon="star" v-if="l.isFavorite"/>
|
||||
<icon :icon="['far', 'star']" v-else/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="title">{{ l.title }}</div>
|
||||
</router-link>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -66,23 +91,25 @@
|
|||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import ListService from '../../services/list'
|
||||
import Fancycheckbox from '../../components/input/fancycheckbox'
|
||||
import {LOADING} from '@/store/mutation-types'
|
||||
import ListCard from '@/components/list/partials/list-card'
|
||||
|
||||
export default {
|
||||
name: 'ListNamespaces',
|
||||
components: {
|
||||
ListCard,
|
||||
Fancycheckbox,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showArchived: false,
|
||||
// listId is the key, the object is the background blob
|
||||
backgrounds: {},
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.showArchived = JSON.parse(localStorage.getItem('showArchived')) ?? false
|
||||
this.loadBackgroundsForLists()
|
||||
},
|
||||
mounted() {
|
||||
this.setTitle(this.$t('namespace.title'))
|
||||
|
@ -94,6 +121,31 @@ export default {
|
|||
loading: LOADING,
|
||||
}),
|
||||
methods: {
|
||||
loadBackgroundsForLists() {
|
||||
const listService = new ListService()
|
||||
this.namespaces.forEach(n => {
|
||||
n.lists.forEach(l => {
|
||||
if (l.backgroundInformation) {
|
||||
listService.background(l)
|
||||
.then(b => {
|
||||
this.$set(this.backgrounds, l.id, b)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
toggleFavoriteList(list) {
|
||||
// The favorites pseudo list is always favorite
|
||||
// Archived lists cannot be marked favorite
|
||||
if (list.id === -1 || list.isArchived) {
|
||||
return
|
||||
}
|
||||
this.$store.dispatch('lists/toggleListFavorite', list)
|
||||
.catch(e => this.error(e))
|
||||
},
|
||||
saveShowArchivedState() {
|
||||
localStorage.setItem('showArchived', JSON.stringify(this.showArchived))
|
||||
},
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
<div class="task-view">
|
||||
<heading v-model="task" :can-write="canWrite" ref="heading"/>
|
||||
<h6 class="subtitle" v-if="parent && parent.namespace && parent.list">
|
||||
{{ getNamespaceTitle(parent.namespace) }} >
|
||||
{{ parent.namespace.title }} >
|
||||
<router-link :to="{ name: listViewName, params: { listId: parent.list.id } }">
|
||||
{{ getListTitle(parent.list) }}
|
||||
{{ parent.list.title }}
|
||||
</router-link>
|
||||
</h6>
|
||||
|
||||
|
|
Loading…
Reference in New Issue