mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 23:06:43 +02:00
feat(todo/web, shared-i18n): complete i18n for Todo web app + add missing common translations
Extract ~120 hardcoded German strings from 14 Svelte components into i18n locale files using svelte-i18n $t() calls. Add new translation sections (taskForm, filters, tags, subtasks, durationPicker, kanban, toolbar) across all 5 languages (de/en/fr/es/it). Also add missing shared common translations for Spanish, French, and Italian (150+ keys each) in packages/shared-i18n. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5c66492279
commit
cb85fba820
30 changed files with 2145 additions and 248 deletions
|
|
@ -4,6 +4,8 @@
|
|||
import { flip } from 'svelte/animate';
|
||||
import { untrack } from 'svelte';
|
||||
import { Check, Plus, X, DotsSixVertical } from '@manacore/shared-icons';
|
||||
import { TodoEvents } from '@manacore/shared-utils/analytics';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
subtasks: Subtask[];
|
||||
|
|
@ -20,7 +22,9 @@
|
|||
const current = subtasks;
|
||||
untrack(() => {
|
||||
const currentIds = new Set(current.map((s) => s.id));
|
||||
const itemIds = new Set(items.filter((i) => i.id !== SHADOW_PLACEHOLDER_ITEM_ID).map((i) => i.id));
|
||||
const itemIds = new Set(
|
||||
items.filter((i) => i.id !== SHADOW_PLACEHOLDER_ITEM_ID).map((i) => i.id)
|
||||
);
|
||||
const idsChanged =
|
||||
currentIds.size !== itemIds.size || current.some((s) => !itemIds.has(s.id));
|
||||
|
||||
|
|
@ -42,10 +46,13 @@
|
|||
items = e.detail.items.filter((item) => item.id !== SHADOW_PLACEHOLDER_ITEM_ID);
|
||||
onChange(items.map((item, index) => ({ ...item, order: index })));
|
||||
dropInProgress = true;
|
||||
setTimeout(() => { dropInProgress = false; }, 500);
|
||||
setTimeout(() => {
|
||||
dropInProgress = false;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function toggleComplete(id: string) {
|
||||
const target = subtasks.find((s) => s.id === id);
|
||||
const updated = subtasks.map((s) =>
|
||||
s.id === id
|
||||
? {
|
||||
|
|
@ -55,6 +62,7 @@
|
|||
}
|
||||
: s
|
||||
);
|
||||
if (target && !target.isCompleted) TodoEvents.subtaskCompleted();
|
||||
onChange(updated);
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +130,7 @@
|
|||
}}
|
||||
>
|
||||
<!-- Drag handle -->
|
||||
<div class="drag-handle" aria-label="Ziehen zum Sortieren">
|
||||
<div class="drag-handle" aria-label={$t('subtasks.dragToSort')}>
|
||||
<DotsSixVertical size={16} />
|
||||
</div>
|
||||
|
||||
|
|
@ -147,15 +155,15 @@
|
|||
spellcheck="false"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => handleTitleKeydown(e, subtask)}
|
||||
onblur={(e) => handleTitleBlur(e, subtask)}
|
||||
>{subtask.title}</span>
|
||||
onblur={(e) => handleTitleBlur(e, subtask)}>{subtask.title}</span
|
||||
>
|
||||
|
||||
<!-- Delete button -->
|
||||
<button
|
||||
type="button"
|
||||
class="subtask-delete"
|
||||
onclick={() => deleteSubtask(subtask.id)}
|
||||
title="Löschen"
|
||||
title={$t('common.delete')}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
|
|
@ -172,12 +180,12 @@
|
|||
<input
|
||||
type="text"
|
||||
class="add-input"
|
||||
placeholder="Subtask hinzufügen..."
|
||||
placeholder={$t('subtasks.addPlaceholder')}
|
||||
bind:value={newSubtaskTitle}
|
||||
onkeydown={handleAddKeydown}
|
||||
/>
|
||||
{#if newSubtaskTitle.trim()}
|
||||
<button type="button" class="add-btn" onclick={addSubtask}> Hinzufügen </button>
|
||||
<button type="button" class="add-btn" onclick={addSubtask}> {$t('subtasks.add')} </button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { DotsThree, Plus, X } from '@manacore/shared-icons';
|
||||
import TagStripModal from './TagStripModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
|
||||
|
|
@ -54,7 +55,7 @@
|
|||
class="clear-filter-pill glass-tag"
|
||||
class:hidden={!hasSelectedTags}
|
||||
onclick={() => viewStore.setFilterLabelIds([])}
|
||||
title="Filter löschen"
|
||||
title={$t('filters.clearFilter')}
|
||||
disabled={!hasSelectedTags}
|
||||
>
|
||||
<X size={16} weight="bold" />
|
||||
|
|
@ -62,15 +63,15 @@
|
|||
</button>
|
||||
|
||||
<!-- More Pill (opens modal) -->
|
||||
<button class="more-pill glass-tag" onclick={handleOpenModal} title="Alle Tags anzeigen">
|
||||
<button class="more-pill glass-tag" onclick={handleOpenModal} title={$t('tags.showAllTags')}>
|
||||
<DotsThree size={18} weight="bold" />
|
||||
<span class="tag-name">Alle Tags</span>
|
||||
<span class="tag-name">{$t('tags.allTags')}</span>
|
||||
</button>
|
||||
|
||||
{#if !hasTags}
|
||||
<button class="empty-state glass-tag" onclick={() => goto('/tags')}>
|
||||
<span>Keine Tags vorhanden</span>
|
||||
<span class="add-hint">+ Erstellen</span>
|
||||
<span>{$t('tags.noTagsAvailable')}</span>
|
||||
<span class="add-hint">{$t('tags.createShort')}</span>
|
||||
</button>
|
||||
{:else}
|
||||
{#each sortedTags as tag (tag.id)}
|
||||
|
|
@ -90,10 +91,10 @@
|
|||
<button
|
||||
class="create-pill glass-tag"
|
||||
onclick={() => goto('/tags?new=true')}
|
||||
title="Neuer Tag"
|
||||
title={$t('tags.newTag')}
|
||||
>
|
||||
<Plus size={16} weight="bold" />
|
||||
<span class="tag-name">Neuer Tag</span>
|
||||
<span class="tag-name">{$t('tags.newTag')}</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import { Plus, X, Check, Pencil, Trash, MagnifyingGlass } from '@manacore/shared-icons';
|
||||
import { TagColorPicker, focusTrap } from '@manacore/shared-ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
|
||||
|
|
@ -156,10 +157,10 @@
|
|||
<div class="modal-header">
|
||||
<h2 class="modal-title">Tags</h2>
|
||||
<div class="header-actions">
|
||||
<button class="header-btn" onclick={openNewTagForm} title="Neuer Tag">
|
||||
<button class="header-btn" onclick={openNewTagForm} title={$t('tags.newTag')}>
|
||||
<Plus size={18} weight="bold" />
|
||||
</button>
|
||||
<button class="header-btn close-btn" onclick={onClose} title="Schließen">
|
||||
<button class="header-btn close-btn" onclick={onClose} title={$t('common.close')}>
|
||||
<X size={18} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -169,10 +170,10 @@
|
|||
<div class="modal-content">
|
||||
{#if tagsCtx.value.length === 0 && !showNewTagForm}
|
||||
<div class="empty-state">
|
||||
<p>Keine Tags vorhanden</p>
|
||||
<p>{$t('tags.noTagsAvailable')}</p>
|
||||
<button class="create-btn" onclick={openNewTagForm}>
|
||||
<Plus size={16} weight="bold" />
|
||||
Tag erstellen
|
||||
{$t('tags.createTag')}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
@ -180,8 +181,8 @@
|
|||
{#if showNewTagForm}
|
||||
<div class="edit-form-section">
|
||||
<div class="edit-form-header">
|
||||
<span class="edit-form-title">Neuer Tag</span>
|
||||
<button class="icon-btn" onclick={closeNewTagForm} title="Abbrechen">
|
||||
<span class="edit-form-title">{$t('tags.newTag')}</span>
|
||||
<button class="icon-btn" onclick={closeNewTagForm} title={$t('common.cancel')}>
|
||||
<X size={14} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -192,7 +193,7 @@
|
|||
type="text"
|
||||
bind:value={newTagName}
|
||||
onkeydown={handleNewTagKeydown}
|
||||
placeholder="Tag Name"
|
||||
placeholder={$t('tags.tagName')}
|
||||
class="name-input"
|
||||
autofocus
|
||||
/>
|
||||
|
|
@ -210,7 +211,7 @@
|
|||
disabled={!newTagName.trim() || isCreatingTag}
|
||||
>
|
||||
<Check size={14} weight="bold" />
|
||||
Erstellen
|
||||
{$t('tags.createTag')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -221,8 +222,8 @@
|
|||
{#if editingTag}
|
||||
<div class="edit-form-section">
|
||||
<div class="edit-form-header">
|
||||
<span class="edit-form-title">Tag bearbeiten</span>
|
||||
<button class="icon-btn" onclick={closeEditTag} title="Abbrechen">
|
||||
<span class="edit-form-title">{$t('tags.editTag')}</span>
|
||||
<button class="icon-btn" onclick={closeEditTag} title={$t('common.cancel')}>
|
||||
<X size={14} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -233,7 +234,7 @@
|
|||
type="text"
|
||||
bind:value={editTagName}
|
||||
onkeydown={handleEditTagKeydown}
|
||||
placeholder="Tag Name"
|
||||
placeholder={$t('tags.tagName')}
|
||||
class="name-input"
|
||||
autofocus
|
||||
/>
|
||||
|
|
@ -245,7 +246,11 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-danger" onclick={handleDeleteTag} title="Tag löschen">
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
onclick={handleDeleteTag}
|
||||
title={$t('tags.deleteTag')}
|
||||
>
|
||||
<Trash size={14} weight="bold" />
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -254,7 +259,7 @@
|
|||
disabled={!editTagName.trim() || isSavingTag}
|
||||
>
|
||||
<Check size={14} weight="bold" />
|
||||
Speichern
|
||||
{$t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -276,7 +281,7 @@
|
|||
<button
|
||||
class="tag-edit-btn"
|
||||
onclick={() => openEditTag(tag)}
|
||||
title="Tag bearbeiten"
|
||||
title={$t('tags.editTag')}
|
||||
>
|
||||
<Pencil size={10} weight="bold" />
|
||||
</button>
|
||||
|
|
@ -286,7 +291,7 @@
|
|||
|
||||
{#if searchQuery && sortedTags.length === 0}
|
||||
<div class="search-empty">
|
||||
<p>Keine Tags gefunden für "{searchQuery}"</p>
|
||||
<p>{$t('tags.noTagsFound', { values: { query: searchQuery } })}</p>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
@ -298,12 +303,16 @@
|
|||
<MagnifyingGlass size={16} class="search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Tags suchen..."
|
||||
placeholder={$t('tags.searchTags')}
|
||||
bind:value={searchQuery}
|
||||
class="search-input"
|
||||
/>
|
||||
{#if searchQuery}
|
||||
<button class="search-clear" onclick={() => (searchQuery = '')} title="Suche leeren">
|
||||
<button
|
||||
class="search-clear"
|
||||
onclick={() => (searchQuery = '')}
|
||||
title={$t('tags.clearSearch')}
|
||||
>
|
||||
<X size={14} weight="bold" />
|
||||
</button>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@
|
|||
DurationPicker,
|
||||
FunRatingPicker,
|
||||
TagSelector,
|
||||
ReminderSelector,
|
||||
} from './form';
|
||||
import { ContactSelector, focusTrap } from '@manacore/shared-ui';
|
||||
import { ManaLinkList, ManaLinkPicker } from '@manacore/shared-links/ui';
|
||||
import { searchCrossApp } from '$lib/data/cross-app-search';
|
||||
import { X, Trash } from '@manacore/shared-icons';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
task: Task;
|
||||
|
|
@ -50,6 +52,7 @@
|
|||
form.isLoading = true;
|
||||
try {
|
||||
onSave(form.buildUpdateInput(task));
|
||||
await form.persistReminder(task.id);
|
||||
} finally {
|
||||
form.isLoading = false;
|
||||
}
|
||||
|
|
@ -84,13 +87,15 @@
|
|||
<div class="top-bar">
|
||||
<div class="top-left">
|
||||
{#if form.showDeleteConfirm}
|
||||
<span class="delete-confirm-text">Wirklich löschen?</span>
|
||||
<button class="btn-ghost-danger" onclick={handleDelete}>Ja, löschen</button>
|
||||
<span class="delete-confirm-text">{$t('taskForm.confirmDelete')}</span>
|
||||
<button class="btn-ghost-danger" onclick={handleDelete}
|
||||
>{$t('taskForm.yesDelete')}</button
|
||||
>
|
||||
<button class="btn-ghost" onclick={() => (form.showDeleteConfirm = false)}
|
||||
>Abbrechen</button
|
||||
>{$t('common.cancel')}</button
|
||||
>
|
||||
{:else}
|
||||
<button class="btn-icon-danger" onclick={handleDelete} title="Aufgabe löschen">
|
||||
<button class="btn-icon-danger" onclick={handleDelete} title={$t('task.deleteTask')}>
|
||||
<Trash size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
|
|
@ -101,9 +106,11 @@
|
|||
onclick={handleSave}
|
||||
disabled={form.isLoading || !form.title.trim()}
|
||||
>
|
||||
{#if form.isLoading}<span class="spinner"></span>{:else}Speichern{/if}
|
||||
{#if form.isLoading}<span class="spinner"></span>{:else}{$t('common.save')}{/if}
|
||||
</button>
|
||||
<button class="btn-close" onclick={onClose} title="Schließen"><X size={18} /></button>
|
||||
<button class="btn-close" onclick={onClose} title={$t('common.close')}
|
||||
><X size={18} /></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -112,7 +119,7 @@
|
|||
<textarea
|
||||
class="title-input"
|
||||
bind:value={form.title}
|
||||
placeholder="Aufgabentitel..."
|
||||
placeholder={$t('taskForm.titlePlaceholder')}
|
||||
rows="1"
|
||||
use:autoGrow
|
||||
></textarea>
|
||||
|
|
@ -121,23 +128,25 @@
|
|||
<!-- Content: Description (left) + Subtasks/Links (right) -->
|
||||
<div class="content-grid">
|
||||
<div class="col-desc">
|
||||
<span class="col-label">Beschreibung</span>
|
||||
<span class="col-label">{$t('taskForm.description')}</span>
|
||||
<textarea
|
||||
class="desc-textarea"
|
||||
bind:value={form.description}
|
||||
placeholder="Beschreibung hinzufügen..."
|
||||
placeholder={$t('taskForm.addDescription')}
|
||||
rows="5"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-subtasks">
|
||||
<span class="col-label">Subtasks</span>
|
||||
<span class="col-label">{$t('taskForm.subtasks')}</span>
|
||||
<SubtaskList subtasks={form.subtasks} onChange={handleSubtasksChange} />
|
||||
|
||||
<div class="links-block">
|
||||
<div class="links-header">
|
||||
<span class="col-label">Verknüpfungen</span>
|
||||
<button class="link-add" onclick={() => (showLinkPicker = true)}>+ Verknüpfen</button>
|
||||
<span class="col-label">{$t('taskForm.links')}</span>
|
||||
<button class="link-add" onclick={() => (showLinkPicker = true)}
|
||||
>{$t('taskForm.addLink')}</button
|
||||
>
|
||||
</div>
|
||||
<ManaLinkList recordRef={{ app: 'todo', collection: 'tasks', id: task.id }} editable />
|
||||
</div>
|
||||
|
|
@ -148,7 +157,7 @@
|
|||
<div class="props-strip">
|
||||
<!-- Status -->
|
||||
<div class="prop">
|
||||
<span class="prop-label">Status</span>
|
||||
<span class="prop-label">{$t('taskForm.status')}</span>
|
||||
<select class="prop-select" bind:value={form.status}>
|
||||
{#each STATUS_OPTIONS as s}
|
||||
<option value={s.value}>{s.label}</option>
|
||||
|
|
@ -160,7 +169,7 @@
|
|||
|
||||
<!-- Priorität -->
|
||||
<div class="prop prop-priority">
|
||||
<span class="prop-label">Priorität</span>
|
||||
<span class="prop-label">{$t('task.priority')}</span>
|
||||
<PrioritySelector value={form.priority} onChange={(p) => (form.priority = p)} />
|
||||
</div>
|
||||
|
||||
|
|
@ -168,25 +177,25 @@
|
|||
|
||||
<!-- Fälligkeit -->
|
||||
<div class="prop">
|
||||
<span class="prop-label">Fälligkeit</span>
|
||||
<span class="prop-label">{$t('taskForm.dueDate')}</span>
|
||||
<input type="date" class="prop-input" bind:value={form.dueDate} />
|
||||
</div>
|
||||
|
||||
<!-- Uhrzeit -->
|
||||
<div class="prop">
|
||||
<span class="prop-label">Uhrzeit</span>
|
||||
<span class="prop-label">{$t('taskForm.time')}</span>
|
||||
<input type="time" class="prop-input" bind:value={form.dueTime} />
|
||||
</div>
|
||||
|
||||
<!-- Startdatum -->
|
||||
<div class="prop">
|
||||
<span class="prop-label">Startdatum</span>
|
||||
<span class="prop-label">{$t('taskForm.startDate')}</span>
|
||||
<input type="date" class="prop-input" bind:value={form.startDate} />
|
||||
</div>
|
||||
|
||||
<!-- Wiederholung -->
|
||||
<div class="prop">
|
||||
<span class="prop-label">Wiederholung</span>
|
||||
<span class="prop-label">{$t('taskForm.recurrence')}</span>
|
||||
<select class="prop-select" bind:value={form.recurrenceRule}>
|
||||
{#each RECURRENCE_OPTIONS as o}
|
||||
<option value={o.value}>{o.label}</option>
|
||||
|
|
@ -194,11 +203,21 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Erinnerung -->
|
||||
<div class="prop">
|
||||
<span class="prop-label">{$t('reminders.label')}</span>
|
||||
<ReminderSelector
|
||||
value={form.reminderMinutes}
|
||||
onChange={(v) => (form.reminderMinutes = v)}
|
||||
disabled={!form.dueDate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="prop-divider"></div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="prop prop-tags">
|
||||
<span class="prop-label">Tags</span>
|
||||
<span class="prop-label">{$t('taskForm.tags')}</span>
|
||||
<TagSelector
|
||||
selectedIds={form.selectedLabelIds}
|
||||
onChange={(ids) => (form.selectedLabelIds = ids)}
|
||||
|
|
@ -209,31 +228,31 @@
|
|||
|
||||
<!-- Zuständig -->
|
||||
<div class="prop prop-contact">
|
||||
<span class="prop-label">Zuständig</span>
|
||||
<span class="prop-label">{$t('taskForm.assignee')}</span>
|
||||
<ContactSelector
|
||||
selectedContacts={form.assignee}
|
||||
onContactsChange={(c) => (form.assignee = c)}
|
||||
onSearch={(q) => contactsStore.searchContacts(q)}
|
||||
singleSelect={true}
|
||||
allowManualEntry={false}
|
||||
placeholder="Zuweisen..."
|
||||
addLabel="Zuweisen"
|
||||
searchPlaceholder="Name oder E-Mail..."
|
||||
placeholder={$t('taskForm.assignPlaceholder')}
|
||||
addLabel={$t('taskForm.assignLabel')}
|
||||
searchPlaceholder={$t('taskForm.nameOrEmail')}
|
||||
isAvailable={form.contactsAvailable ?? false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Beteiligte -->
|
||||
<div class="prop prop-contact">
|
||||
<span class="prop-label">Beteiligte</span>
|
||||
<span class="prop-label">{$t('taskForm.involved')}</span>
|
||||
<ContactSelector
|
||||
selectedContacts={form.involvedContacts}
|
||||
onContactsChange={(c) => (form.involvedContacts = c)}
|
||||
onSearch={(q) => contactsStore.searchContacts(q)}
|
||||
allowManualEntry={false}
|
||||
placeholder="Hinzufügen..."
|
||||
addLabel="Hinzufügen"
|
||||
searchPlaceholder="Name oder E-Mail..."
|
||||
placeholder={$t('taskForm.addPlaceholder')}
|
||||
addLabel={$t('taskForm.addLabel')}
|
||||
searchPlaceholder={$t('taskForm.nameOrEmail')}
|
||||
isAvailable={form.contactsAvailable ?? false}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -242,13 +261,13 @@
|
|||
|
||||
<!-- Storypoints -->
|
||||
<div class="prop">
|
||||
<span class="prop-label">Storypoints</span>
|
||||
<span class="prop-label">{$t('taskForm.storypoints')}</span>
|
||||
<StorypointsSelector value={form.storyPoints} onChange={(v) => (form.storyPoints = v)} />
|
||||
</div>
|
||||
|
||||
<!-- Effektive Dauer -->
|
||||
<div class="prop">
|
||||
<span class="prop-label">Dauer</span>
|
||||
<span class="prop-label">{$t('taskForm.duration')}</span>
|
||||
<DurationPicker
|
||||
value={form.effectiveDuration}
|
||||
onChange={(v) => (form.effectiveDuration = v)}
|
||||
|
|
@ -257,7 +276,8 @@
|
|||
|
||||
<!-- Spaß-Faktor -->
|
||||
<div class="prop">
|
||||
<span class="prop-label">Spaß{form.funRating !== null ? ` (${form.funRating})` : ''}</span
|
||||
<span class="prop-label"
|
||||
>{$t('taskForm.fun')}{form.funRating !== null ? ` (${form.funRating})` : ''}</span
|
||||
>
|
||||
<FunRatingPicker value={form.funRating} onChange={(v) => (form.funRating = v)} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
import type { SortBy, SortOrder } from '$lib/stores/view.svelte';
|
||||
import { CaretDown, Check, CheckCircle, MagnifyingGlass, X } from '@manacore/shared-icons';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
// Layout
|
||||
|
|
@ -61,18 +62,38 @@
|
|||
onToggleCompleted,
|
||||
}: Props = $props();
|
||||
|
||||
const priorities: { value: TaskPriority; label: string; color: string; bgColor: string }[] = [
|
||||
{ value: 'urgent', label: 'Dringend', color: '#ef4444', bgColor: 'bg-red-500' },
|
||||
{ value: 'high', label: 'Hoch', color: '#f97316', bgColor: 'bg-orange-500' },
|
||||
{ value: 'medium', label: 'Normal', color: '#eab308', bgColor: 'bg-yellow-500' },
|
||||
{ value: 'low', label: 'Niedrig', color: '#3b82f6', bgColor: 'bg-blue-500' },
|
||||
];
|
||||
let priorities = $derived([
|
||||
{
|
||||
value: 'urgent' as TaskPriority,
|
||||
label: $t('priority.urgent'),
|
||||
color: '#ef4444',
|
||||
bgColor: 'bg-red-500',
|
||||
},
|
||||
{
|
||||
value: 'high' as TaskPriority,
|
||||
label: $t('priority.high'),
|
||||
color: '#f97316',
|
||||
bgColor: 'bg-orange-500',
|
||||
},
|
||||
{
|
||||
value: 'medium' as TaskPriority,
|
||||
label: $t('priority.medium'),
|
||||
color: '#eab308',
|
||||
bgColor: 'bg-yellow-500',
|
||||
},
|
||||
{
|
||||
value: 'low' as TaskPriority,
|
||||
label: $t('priority.low'),
|
||||
color: '#3b82f6',
|
||||
bgColor: 'bg-blue-500',
|
||||
},
|
||||
]);
|
||||
|
||||
const sortOptions: { id: SortBy; label: string }[] = [
|
||||
{ id: 'dueDate', label: 'Datum' },
|
||||
{ id: 'priority', label: 'Priorit.' },
|
||||
{ id: 'title', label: 'Name' },
|
||||
];
|
||||
let sortOptions = $derived([
|
||||
{ id: 'dueDate' as SortBy, label: $t('filters.date') },
|
||||
{ id: 'priority' as SortBy, label: $t('filters.priorityShort') },
|
||||
{ id: 'title' as SortBy, label: $t('filters.name') },
|
||||
]);
|
||||
|
||||
// Dropdown states
|
||||
let showLabelsDropdown = $state(false);
|
||||
|
|
@ -107,7 +128,7 @@
|
|||
class="clear-filter-pill glass-pill"
|
||||
class:hidden={!hasActiveFilters}
|
||||
onclick={onClearFilters}
|
||||
title="Filter löschen"
|
||||
title={$t('filters.clearFilter')}
|
||||
disabled={!hasActiveFilters}
|
||||
>
|
||||
<X size={16} weight="bold" />
|
||||
|
|
@ -116,7 +137,11 @@
|
|||
|
||||
<!-- Tag Chips -->
|
||||
{#if showTags}
|
||||
<button class="label-pill glass-pill" onclick={() => goto('/tags')} title="Tags verwalten">
|
||||
<button
|
||||
class="label-pill glass-pill"
|
||||
onclick={() => goto('/tags')}
|
||||
title={$t('filters.manageTags')}
|
||||
>
|
||||
<span class="pill-label label-text">Tags:</span>
|
||||
</button>
|
||||
{#if tagsCtx.value.length > 0}
|
||||
|
|
@ -163,7 +188,7 @@
|
|||
class="sort-pill glass-pill"
|
||||
class:active={sortBy === option.id}
|
||||
onclick={() => onSortChange(option.id)}
|
||||
title="Nach {option.label} sortieren"
|
||||
title={$t('tags.sortBy', { values: { field: option.label } })}
|
||||
>
|
||||
<span class="pill-label">{option.label}</span>
|
||||
</button>
|
||||
|
|
@ -176,10 +201,10 @@
|
|||
class="glass-pill"
|
||||
class:active={isCompletedVisible}
|
||||
onclick={onToggleCompleted}
|
||||
title={isCompletedVisible ? 'Erledigte ausblenden' : 'Erledigte anzeigen'}
|
||||
title={isCompletedVisible ? $t('filters.hideCompleted') : $t('filters.showCompleted')}
|
||||
>
|
||||
<CheckCircle size={20} class="pill-icon" />
|
||||
<span class="pill-label">Erledigt</span>
|
||||
<span class="pill-label">{$t('nav.completed')}</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -200,7 +225,7 @@
|
|||
type="text"
|
||||
value={searchQuery}
|
||||
oninput={(e) => onSearchChange(e.currentTarget.value)}
|
||||
placeholder="Aufgaben suchen..."
|
||||
placeholder={$t('filters.searchTasks')}
|
||||
class="w-full pl-10 pr-8 py-2 text-sm bg-background border border-border rounded-lg outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary placeholder:text-muted-foreground transition-all"
|
||||
/>
|
||||
{#if searchQuery}
|
||||
|
|
@ -219,7 +244,7 @@
|
|||
onclick={onClearFilters}
|
||||
>
|
||||
<X size={16} />
|
||||
Zurücksetzen
|
||||
{$t('filters.resetFilters')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -230,7 +255,7 @@
|
|||
<!-- Priority filters -->
|
||||
<div class="filter-group flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-muted-foreground uppercase tracking-wide"
|
||||
>Priorität</span
|
||||
>{$t('task.priority')}</span
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
{#each priorities as priority}
|
||||
|
|
@ -277,7 +302,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-muted-foreground">Auswählen</span>
|
||||
<span class="text-muted-foreground">{$t('filters.select')}</span>
|
||||
{/if}
|
||||
<CaretDown
|
||||
size={16}
|
||||
|
|
@ -294,7 +319,9 @@
|
|||
class="absolute top-full left-0 mt-2 z-50 min-w-[220px] bg-popover border border-border rounded-xl shadow-lg p-2 animate-in fade-in slide-in-from-top-2 duration-150"
|
||||
>
|
||||
{#if tagsCtx.value.length === 0}
|
||||
<p class="text-sm text-muted-foreground p-3 text-center">Keine Tags vorhanden</p>
|
||||
<p class="text-sm text-muted-foreground p-3 text-center">
|
||||
{$t('filters.noTagsAvailable')}
|
||||
</p>
|
||||
{:else}
|
||||
<div class="max-h-[200px] overflow-y-auto">
|
||||
{#each tagsCtx.value as label}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,11 @@
|
|||
DurationPicker,
|
||||
FunRatingPicker,
|
||||
TagSelector,
|
||||
ReminderSelector,
|
||||
} from './form';
|
||||
import { PRIORITY_COLORS } from '$lib/constants/priority';
|
||||
import { TodoEvents } from '@manacore/shared-utils/analytics';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
task: Task;
|
||||
|
|
@ -124,6 +127,7 @@
|
|||
form.funRating,
|
||||
form.assignee,
|
||||
form.involvedContacts,
|
||||
form.reminderMinutes,
|
||||
];
|
||||
scheduleAutoSave();
|
||||
});
|
||||
|
|
@ -208,6 +212,7 @@
|
|||
try {
|
||||
const data = form.buildUpdateInput(task);
|
||||
onSave(data);
|
||||
await form.persistReminder(task.id);
|
||||
} finally {
|
||||
form.isLoading = false;
|
||||
}
|
||||
|
|
@ -228,6 +233,7 @@
|
|||
function toggleSubtask(subtaskId: string) {
|
||||
if (!onSave) return;
|
||||
const subtasks = $state.snapshot(task.subtasks) ?? [];
|
||||
const target = subtasks.find((s) => s.id === subtaskId);
|
||||
const updated = subtasks.map((s) =>
|
||||
s.id === subtaskId
|
||||
? {
|
||||
|
|
@ -237,6 +243,7 @@
|
|||
}
|
||||
: s
|
||||
);
|
||||
if (target && !target.isCompleted) TodoEvents.subtaskCompleted();
|
||||
onSave({ subtasks: updated });
|
||||
}
|
||||
|
||||
|
|
@ -343,11 +350,7 @@
|
|||
/>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<span
|
||||
class="task-title"
|
||||
class:line-through={task.isCompleted}
|
||||
onclick={startTitleEdit}
|
||||
>
|
||||
<span class="task-title" class:line-through={task.isCompleted} onclick={startTitleEdit}>
|
||||
{task.title}
|
||||
</span>
|
||||
{/if}
|
||||
|
|
@ -371,7 +374,12 @@
|
|||
{#if task.metadata?.assignee || (task.metadata?.involvedContacts && task.metadata.involvedContacts.length > 0)}
|
||||
<div class="contacts-display">
|
||||
{#if task.metadata?.assignee}
|
||||
<div class="assignee-avatar" title="Zuständig: {task.metadata.assignee.displayName}">
|
||||
<div
|
||||
class="assignee-avatar"
|
||||
title={$t('kanban.assignedTo', {
|
||||
values: { name: task.metadata.assignee.displayName },
|
||||
})}
|
||||
>
|
||||
<ContactAvatar
|
||||
name={task.metadata.assignee.displayName}
|
||||
photoUrl={task.metadata.assignee.photoUrl}
|
||||
|
|
@ -382,7 +390,10 @@
|
|||
{#if task.metadata?.involvedContacts && task.metadata.involvedContacts.length > 0}
|
||||
<div class="involved-avatars">
|
||||
{#each task.metadata.involvedContacts.slice(0, 2) as contact}
|
||||
<div class="involved-avatar" title="Beteiligt: {contact.displayName}">
|
||||
<div
|
||||
class="involved-avatar"
|
||||
title={$t('kanban.involvedContact', { values: { name: contact.displayName } })}
|
||||
>
|
||||
<ContactAvatar name={contact.displayName} photoUrl={contact.photoUrl} size="xs" />
|
||||
</div>
|
||||
{/each}
|
||||
|
|
@ -403,15 +414,15 @@
|
|||
e.stopPropagation();
|
||||
showCreatedDate = !showCreatedDate;
|
||||
}}
|
||||
title="Klicken für Erstellungsdatum"
|
||||
title={$t('taskForm.clickForCreatedDate')}
|
||||
>
|
||||
{#if showCreatedDate}
|
||||
<span class="date-label">Erstellt</span>
|
||||
<span class="date-label">{$t('taskForm.created')}</span>
|
||||
<span class="date-value"
|
||||
>{format(new Date(task.createdAt), 'd. MMM yyyy', { locale: de })}</span
|
||||
>
|
||||
{/if}
|
||||
<span class="date-label">Erledigt</span>
|
||||
<span class="date-label">{$t('taskForm.completed')}</span>
|
||||
<span class="date-value"
|
||||
>{format(new Date(task.completedAt), 'd. MMM yyyy', { locale: de })}</span
|
||||
>
|
||||
|
|
@ -431,7 +442,7 @@
|
|||
type="button"
|
||||
class="detail-btn"
|
||||
onclick={handleOpenModal}
|
||||
title="Details öffnen"
|
||||
title={$t('taskForm.openDetails')}
|
||||
tabindex="-1"
|
||||
>
|
||||
<ArrowsOutSimple size={14} />
|
||||
|
|
@ -470,35 +481,37 @@
|
|||
<div class="expanded-form">
|
||||
<!-- Title -->
|
||||
<div class="form-section">
|
||||
<label class="form-label" for="task-title-{task.id}">Titel</label>
|
||||
<label class="form-label" for="task-title-{task.id}">{$t('task.title')}</label>
|
||||
<input
|
||||
bind:this={titleInputRef}
|
||||
id="task-title-{task.id}"
|
||||
type="text"
|
||||
class="form-input"
|
||||
bind:value={form.title}
|
||||
placeholder="Aufgabentitel..."
|
||||
placeholder={$t('taskForm.titlePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="form-section">
|
||||
<label class="form-label" for="task-description-{task.id}">Beschreibung</label>
|
||||
<label class="form-label" for="task-description-{task.id}"
|
||||
>{$t('taskForm.description')}</label
|
||||
>
|
||||
<textarea
|
||||
id="task-description-{task.id}"
|
||||
class="form-textarea"
|
||||
bind:value={form.description}
|
||||
placeholder="Beschreibung hinzufügen..."
|
||||
placeholder={$t('taskForm.addDescription')}
|
||||
rows="2"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Time planning row -->
|
||||
<div class="form-section">
|
||||
<label class="form-label">Zeitplanung</label>
|
||||
<label class="form-label">{$t('taskForm.scheduling')}</label>
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label class="form-sublabel" for="due-date-{task.id}">Fällig</label>
|
||||
<label class="form-sublabel" for="due-date-{task.id}">{$t('taskForm.due')}</label>
|
||||
<input
|
||||
id="due-date-{task.id}"
|
||||
type="date"
|
||||
|
|
@ -507,7 +520,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-sublabel" for="due-time-{task.id}">Uhrzeit</label>
|
||||
<label class="form-sublabel" for="due-time-{task.id}">{$t('taskForm.time')}</label>
|
||||
<input
|
||||
id="due-time-{task.id}"
|
||||
type="time"
|
||||
|
|
@ -516,7 +529,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-sublabel" for="start-date-{task.id}">Start</label>
|
||||
<label class="form-sublabel" for="start-date-{task.id}">{$t('taskForm.start')}</label>
|
||||
<input
|
||||
id="start-date-{task.id}"
|
||||
type="date"
|
||||
|
|
@ -529,13 +542,13 @@
|
|||
|
||||
<!-- Priority -->
|
||||
<div class="form-section">
|
||||
<label class="form-label">Priorität</label>
|
||||
<label class="form-label">{$t('task.priority')}</label>
|
||||
<PrioritySelector value={form.priority} onChange={(p) => (form.priority = p)} />
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="form-section">
|
||||
<label class="form-label" for="task-status-{task.id}">Status</label>
|
||||
<label class="form-label" for="task-status-{task.id}">{$t('taskForm.status')}</label>
|
||||
<select id="task-status-{task.id}" class="form-select" bind:value={form.status}>
|
||||
{#each STATUS_OPTIONS as s}
|
||||
<option value={s.value}>{s.label}</option>
|
||||
|
|
@ -545,7 +558,7 @@
|
|||
|
||||
<!-- Tags -->
|
||||
<div class="form-section">
|
||||
<label class="form-label">Tags</label>
|
||||
<label class="form-label">{$t('taskForm.tags')}</label>
|
||||
<TagSelector
|
||||
selectedIds={form.selectedLabelIds}
|
||||
onChange={(ids) => (form.selectedLabelIds = ids)}
|
||||
|
|
@ -554,13 +567,14 @@
|
|||
|
||||
<!-- Subtasks -->
|
||||
<div class="form-section">
|
||||
<label class="form-label">Subtasks</label>
|
||||
<label class="form-label">{$t('taskForm.subtasks')}</label>
|
||||
<SubtaskList subtasks={form.subtasks} onChange={handleSubtasksChange} />
|
||||
</div>
|
||||
|
||||
<!-- Recurrence -->
|
||||
<div class="form-section">
|
||||
<label class="form-label" for="task-recurrence-{task.id}">Wiederholung</label>
|
||||
<label class="form-label" for="task-recurrence-{task.id}">{$t('taskForm.recurrence')}</label
|
||||
>
|
||||
<select id="task-recurrence-{task.id}" class="form-select" bind:value={form.recurrenceRule}>
|
||||
{#each RECURRENCE_OPTIONS as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
|
|
@ -568,33 +582,43 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Reminder -->
|
||||
<div class="form-section">
|
||||
<label class="form-label">{$t('reminders.label')}</label>
|
||||
<ReminderSelector
|
||||
value={form.reminderMinutes}
|
||||
onChange={(v) => (form.reminderMinutes = v)}
|
||||
disabled={!form.dueDate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Contacts: Assignee -->
|
||||
<div class="form-section">
|
||||
<label class="form-label">Zuständig</label>
|
||||
<label class="form-label">{$t('taskForm.assignee')}</label>
|
||||
<ContactSelector
|
||||
selectedContacts={form.assignee}
|
||||
onContactsChange={(contacts) => (form.assignee = contacts)}
|
||||
onSearch={(q) => contactsStore.searchContacts(q)}
|
||||
singleSelect={true}
|
||||
allowManualEntry={false}
|
||||
placeholder="Person zuweisen..."
|
||||
addLabel="Zuweisen"
|
||||
searchPlaceholder="Name oder E-Mail..."
|
||||
placeholder={$t('taskForm.assignPerson')}
|
||||
addLabel={$t('taskForm.assignLabel')}
|
||||
searchPlaceholder={$t('taskForm.nameOrEmail')}
|
||||
isAvailable={form.contactsAvailable ?? false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Contacts: Involved -->
|
||||
<div class="form-section">
|
||||
<label class="form-label">Beteiligte</label>
|
||||
<label class="form-label">{$t('taskForm.involved')}</label>
|
||||
<ContactSelector
|
||||
selectedContacts={form.involvedContacts}
|
||||
onContactsChange={(contacts) => (form.involvedContacts = contacts)}
|
||||
onSearch={(q) => contactsStore.searchContacts(q)}
|
||||
allowManualEntry={false}
|
||||
placeholder="Personen hinzufügen..."
|
||||
addLabel="Person hinzufügen"
|
||||
searchPlaceholder="Name oder E-Mail..."
|
||||
placeholder={$t('taskForm.addPeoplePlaceholder')}
|
||||
addLabel={$t('taskForm.addPersonLabel')}
|
||||
searchPlaceholder={$t('taskForm.nameOrEmail')}
|
||||
isAvailable={form.contactsAvailable ?? false}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -602,18 +626,18 @@
|
|||
<!-- Story Points & Duration & Fun Rating row -->
|
||||
<div class="form-row-3">
|
||||
<div class="form-section">
|
||||
<label class="form-label">Storypoints</label>
|
||||
<label class="form-label">{$t('taskForm.storypoints')}</label>
|
||||
<StorypointsSelector value={form.storyPoints} onChange={(v) => (form.storyPoints = v)} />
|
||||
</div>
|
||||
<div class="form-section">
|
||||
<label class="form-label">Dauer</label>
|
||||
<label class="form-label">{$t('taskForm.duration')}</label>
|
||||
<DurationPicker
|
||||
value={form.effectiveDuration}
|
||||
onChange={(v) => (form.effectiveDuration = v)}
|
||||
/>
|
||||
</div>
|
||||
<div class="form-section">
|
||||
<label class="form-label">Spaß</label>
|
||||
<label class="form-label">{$t('taskForm.fun')}</label>
|
||||
<FunRatingPicker value={form.funRating} onChange={(v) => (form.funRating = v)} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -626,7 +650,7 @@
|
|||
onclick={handleDeleteClick}
|
||||
disabled={form.isLoading}
|
||||
>
|
||||
{form.showDeleteConfirm ? 'Wirklich löschen?' : 'Löschen'}
|
||||
{form.showDeleteConfirm ? $t('taskForm.confirmDelete') : $t('common.delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { ExpandableToolbar } from '@manacore/shared-ui';
|
||||
import TodoToolbarContent from './TodoToolbarContent.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
isCollapsed?: boolean;
|
||||
|
|
@ -15,8 +16,8 @@
|
|||
{isCollapsed}
|
||||
{onCollapsedChange}
|
||||
{bottomOffset}
|
||||
collapsedTitle="Aufgaben-Optionen"
|
||||
expandedTitle="Schließen"
|
||||
collapsedTitle={$t('toolbar.taskOptions')}
|
||||
expandedTitle={$t('common.close')}
|
||||
>
|
||||
{#snippet collapsedIcon()}
|
||||
<!-- Task/list icon -->
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import { todoSettings } from '$lib/stores/settings.svelte';
|
||||
import { PillToolbarButton, PillToolbarDivider, PillViewSwitcher } from '@manacore/shared-ui';
|
||||
import { CheckCircle, Columns, Funnel } from '@manacore/shared-icons';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
/** Vertical layout (for sidebar mode) */
|
||||
|
|
@ -32,19 +33,19 @@
|
|||
let selectedPriorityFilters = $state<TaskPriority[]>([]);
|
||||
let selectedLabelFilters = $state<string[]>([]);
|
||||
|
||||
const priorities: { value: TaskPriority; label: string; color: string }[] = [
|
||||
{ value: 'urgent', label: 'Dringend', color: '#ef4444' },
|
||||
{ value: 'high', label: 'Hoch', color: '#f97316' },
|
||||
{ value: 'medium', label: 'Normal', color: '#eab308' },
|
||||
{ value: 'low', label: 'Niedrig', color: '#3b82f6' },
|
||||
];
|
||||
let priorities = $derived([
|
||||
{ value: 'urgent' as TaskPriority, label: $t('priority.urgent'), color: '#ef4444' },
|
||||
{ value: 'high' as TaskPriority, label: $t('priority.high'), color: '#f97316' },
|
||||
{ value: 'medium' as TaskPriority, label: $t('priority.medium'), color: '#eab308' },
|
||||
{ value: 'low' as TaskPriority, label: $t('priority.low'), color: '#3b82f6' },
|
||||
]);
|
||||
|
||||
// Sort options
|
||||
const sortOptions = [
|
||||
{ id: 'dueDate', label: 'Datum', title: 'Nach Fälligkeitsdatum sortieren' },
|
||||
{ id: 'priority', label: 'Priorität', title: 'Nach Priorität sortieren' },
|
||||
{ id: 'title', label: 'Name', title: 'Alphabetisch sortieren' },
|
||||
];
|
||||
let sortOptions = $derived([
|
||||
{ id: 'dueDate', label: $t('filters.date'), title: $t('filters.sortByDueDate') },
|
||||
{ id: 'priority', label: $t('task.priority'), title: $t('filters.sortByPriority') },
|
||||
{ id: 'title', label: $t('filters.name'), title: $t('filters.sortAlphabetical') },
|
||||
]);
|
||||
|
||||
// Count active filters
|
||||
let activeFilterCount = $derived(selectedPriorityFilters.length + selectedLabelFilters.length);
|
||||
|
|
@ -81,7 +82,7 @@
|
|||
const idx = modes.indexOf(todoSettings.activeLayoutMode);
|
||||
todoSettings.set('activeLayoutMode', modes[(idx + 1) % modes.length]);
|
||||
}}
|
||||
title="Ansicht wechseln"
|
||||
title={$t('toolbar.switchView')}
|
||||
>
|
||||
<Columns size={20} />
|
||||
</PillToolbarButton>
|
||||
|
|
@ -108,7 +109,7 @@
|
|||
{#if showFilterDropdown}
|
||||
<div class="filter-dropdown" class:vertical onclick={(e) => e.stopPropagation()}>
|
||||
<div class="filter-section">
|
||||
<div class="filter-section-header">Priorität</div>
|
||||
<div class="filter-section-header">{$t('task.priority')}</div>
|
||||
<div class="filter-chips">
|
||||
{#each priorities as priority}
|
||||
<button
|
||||
|
|
@ -126,7 +127,7 @@
|
|||
|
||||
{#if activeFilterCount > 0}
|
||||
<button type="button" class="clear-filters-btn" onclick={clearAllFilters}>
|
||||
Filter zurücksetzen
|
||||
{$t('filters.resetAll')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -153,7 +154,7 @@
|
|||
<PillToolbarButton
|
||||
onclick={onToggleShowCompleted}
|
||||
active={showCompleted}
|
||||
title={showCompleted ? 'Erledigte ausblenden' : 'Erledigte anzeigen'}
|
||||
title={showCompleted ? $t('filters.hideCompleted') : $t('filters.showCompleted')}
|
||||
>
|
||||
<CheckCircle size={20} />
|
||||
</PillToolbarButton>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { DurationUnit, EffectiveDuration } from '@todo/shared';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { X } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -24,11 +25,11 @@
|
|||
{ label: '2d', value: 2, unit: 'days' },
|
||||
];
|
||||
|
||||
const unitOptions: { value: DurationUnit; label: string }[] = [
|
||||
{ value: 'minutes', label: 'Minuten' },
|
||||
{ value: 'hours', label: 'Stunden' },
|
||||
{ value: 'days', label: 'Tage' },
|
||||
];
|
||||
let unitOptions = $derived([
|
||||
{ value: 'minutes' as DurationUnit, label: $t('durationPicker.minutes') },
|
||||
{ value: 'hours' as DurationUnit, label: $t('durationPicker.hours') },
|
||||
{ value: 'days' as DurationUnit, label: $t('durationPicker.days') },
|
||||
]);
|
||||
|
||||
// Sync custom inputs with value prop
|
||||
$effect(() => {
|
||||
|
|
@ -96,7 +97,7 @@
|
|||
...
|
||||
</button>
|
||||
{#if value !== null}
|
||||
<button type="button" class="duration-clear" onclick={clear} title="Zurücksetzen">
|
||||
<button type="button" class="duration-clear" onclick={clear} title={$t('common.reset')}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
|
|
@ -109,7 +110,7 @@
|
|||
class="duration-input"
|
||||
bind:value={customValue}
|
||||
oninput={handleCustomChange}
|
||||
placeholder="Wert"
|
||||
placeholder={$t('durationPicker.value')}
|
||||
min="1"
|
||||
/>
|
||||
<select class="duration-unit" bind:value={customUnit} onchange={handleCustomChange}>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { t } from 'svelte-i18n';
|
||||
import { X } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -39,7 +40,12 @@
|
|||
</button>
|
||||
{/each}
|
||||
{#if value !== null}
|
||||
<button type="button" class="fun-rating-clear" onclick={handleClear} title="Zurücksetzen">
|
||||
<button
|
||||
type="button"
|
||||
class="fun-rating-clear"
|
||||
onclick={handleClear}
|
||||
title={$t('common.reset')}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { t } from 'svelte-i18n';
|
||||
import { X } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -32,7 +33,7 @@
|
|||
</button>
|
||||
{/each}
|
||||
{#if value !== null}
|
||||
<button type="button" class="storypoint-clear" onclick={handleClear} title="Zurücksetzen">
|
||||
<button type="button" class="storypoint-clear" onclick={handleClear} title={$t('common.reset')}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { CaretDown, Check } from '@manacore/shared-icons';
|
||||
|
||||
|
|
@ -37,7 +38,7 @@
|
|||
<div class="tag-selector">
|
||||
<button type="button" class="tag-trigger" onclick={handleTriggerClick}>
|
||||
{#if selectedIds.length === 0}
|
||||
<span class="text-muted">Tags auswählen...</span>
|
||||
<span class="text-muted">{$t('tags.selectTags')}</span>
|
||||
{:else}
|
||||
<div class="selected-tags">
|
||||
{#each selectedIds.slice(0, 3) as tagId}
|
||||
|
|
@ -75,7 +76,7 @@
|
|||
</button>
|
||||
{/each}
|
||||
{#if tagsCtx.value.length === 0}
|
||||
<div class="no-tags">Keine Tags vorhanden</div>
|
||||
<div class="no-tags">{$t('tags.noTagsAvailable')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { Task, Subtask } from '@todo/shared';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { isToday, isPast } from 'date-fns';
|
||||
import { formatDueDate } from '$lib/utils/date-display';
|
||||
import { getSubtaskProgress } from '$lib/utils/task-helpers';
|
||||
|
|
@ -166,7 +167,9 @@
|
|||
const current = task.subtasks ?? [];
|
||||
untrack(() => {
|
||||
const newIds = new Set(current.map((s) => s.id));
|
||||
const oldIds = new Set(subtaskItems.filter((s) => s.id !== SHADOW_PLACEHOLDER_ITEM_ID).map((s) => s.id));
|
||||
const oldIds = new Set(
|
||||
subtaskItems.filter((s) => s.id !== SHADOW_PLACEHOLDER_ITEM_ID).map((s) => s.id)
|
||||
);
|
||||
const idsChanged = newIds.size !== oldIds.size || current.some((s) => !oldIds.has(s.id));
|
||||
if (idsChanged) {
|
||||
subtaskItems = [...current];
|
||||
|
|
@ -186,7 +189,9 @@
|
|||
subtaskItems = e.detail.items.filter((s) => s.id !== SHADOW_PLACEHOLDER_ITEM_ID);
|
||||
onSave?.({ subtasks: subtaskItems });
|
||||
subtaskDropInProgress = true;
|
||||
setTimeout(() => { subtaskDropInProgress = false; }, 500);
|
||||
setTimeout(() => {
|
||||
subtaskDropInProgress = false;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function toggleSubtask(subtaskId: string) {
|
||||
|
|
@ -244,8 +249,8 @@
|
|||
spellcheck="false"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={handleTitleKeydown}
|
||||
onblur={handleTitleBlur}
|
||||
>{task.title}</span>
|
||||
onblur={handleTitleBlur}>{task.title}</span
|
||||
>
|
||||
|
||||
<!-- Meta info -->
|
||||
{#if dueDateText() || subtaskProgress() || (task.labels && task.labels.length > 0)}
|
||||
|
|
@ -283,7 +288,7 @@
|
|||
type="button"
|
||||
class="detail-btn"
|
||||
onclick={handleOpenModal}
|
||||
title="Details öffnen"
|
||||
title={$t('taskForm.openDetails')}
|
||||
tabindex="-1"
|
||||
>
|
||||
<ArrowsOutSimple size={14} />
|
||||
|
|
@ -293,7 +298,10 @@
|
|||
{#if task.metadata?.assignee || (task.metadata?.involvedContacts && task.metadata.involvedContacts.length > 0)}
|
||||
<div class="contacts-display">
|
||||
{#if task.metadata?.assignee}
|
||||
<div class="assignee-avatar" title="Zuständig: {task.metadata.assignee.displayName}">
|
||||
<div
|
||||
class="assignee-avatar"
|
||||
title={$t('kanban.assignedTo', { values: { name: task.metadata.assignee.displayName } })}
|
||||
>
|
||||
<ContactAvatar
|
||||
name={task.metadata.assignee.displayName}
|
||||
photoUrl={task.metadata.assignee.photoUrl}
|
||||
|
|
@ -304,7 +312,10 @@
|
|||
{#if task.metadata?.involvedContacts && task.metadata.involvedContacts.length > 0}
|
||||
<div class="involved-avatars">
|
||||
{#each task.metadata.involvedContacts.slice(0, 2) as contact}
|
||||
<div class="involved-avatar" title="Beteiligt: {contact.displayName}">
|
||||
<div
|
||||
class="involved-avatar"
|
||||
title={$t('kanban.involvedContact', { values: { name: contact.displayName } })}
|
||||
>
|
||||
<ContactAvatar name={contact.displayName} photoUrl={contact.photoUrl} size="xs" />
|
||||
</div>
|
||||
{/each}
|
||||
|
|
@ -322,7 +333,12 @@
|
|||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="subtasks-inline"
|
||||
use:dndzone={{ items: subtaskItems, flipDurationMs: 150, dropTargetStyle: {}, type: 'subtask-inline' }}
|
||||
use:dndzone={{
|
||||
items: subtaskItems,
|
||||
flipDurationMs: 150,
|
||||
dropTargetStyle: {},
|
||||
type: 'subtask-inline',
|
||||
}}
|
||||
onconsider={handleSubtaskConsider}
|
||||
onfinalize={handleSubtaskFinalize}
|
||||
>
|
||||
|
|
@ -363,16 +379,16 @@
|
|||
>
|
||||
<button class="context-item" onclick={handleContextEdit}>
|
||||
<Note size={20} class="context-icon" />
|
||||
Bearbeiten
|
||||
{$t('kanban.edit')}
|
||||
</button>
|
||||
<button class="context-item" onclick={handleContextToggleComplete}>
|
||||
<ArrowsClockwise size={20} class="context-icon" />
|
||||
{task.isCompleted ? 'Wiederherstellen' : 'Erledigen'}
|
||||
{task.isCompleted ? $t('kanban.restore') : $t('kanban.complete')}
|
||||
</button>
|
||||
<div class="context-divider"></div>
|
||||
<button class="context-item danger" onclick={handleContextDelete}>
|
||||
<Trash size={20} class="context-icon" />
|
||||
Löschen
|
||||
{$t('common.delete')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -392,10 +408,10 @@
|
|||
onClose={() => (showDeleteConfirm = false)}
|
||||
onConfirm={confirmDelete}
|
||||
variant="danger"
|
||||
title="Aufgabe löschen?"
|
||||
message="Diese Aufgabe wird unwiderruflich gelöscht."
|
||||
confirmLabel="Löschen"
|
||||
cancelLabel="Abbrechen"
|
||||
title={$t('kanban.deleteTitle')}
|
||||
message={$t('kanban.deleteMessage')}
|
||||
confirmLabel={$t('common.delete')}
|
||||
cancelLabel={$t('common.cancel')}
|
||||
/>
|
||||
|
||||
<style>
|
||||
|
|
@ -567,7 +583,6 @@
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
||||
/* Meta info */
|
||||
.task-meta {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { t } from 'svelte-i18n';
|
||||
import { Plus, X } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -57,7 +58,7 @@
|
|||
onclick={handleSubmit}
|
||||
>
|
||||
<Plus size={14} />
|
||||
Hinzufügen
|
||||
{$t('kanban.add')}
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted rounded-full transition-colors"
|
||||
|
|
@ -81,7 +82,7 @@
|
|||
>
|
||||
<Plus size={14} />
|
||||
</div>
|
||||
<span class="group-hover:text-foreground transition-colors">Aufgabe hinzufügen</span>
|
||||
<span class="group-hover:text-foreground transition-colors">{$t('kanban.addTask')}</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -83,7 +83,9 @@
|
|||
"error": "Fehler",
|
||||
"success": "Erfolgreich",
|
||||
"loading": "Laden...",
|
||||
"noResults": "Keine Ergebnisse"
|
||||
"noResults": "Keine Ergebnisse",
|
||||
"reset": "Zurücksetzen",
|
||||
"filter": "Filter"
|
||||
},
|
||||
"errors": {
|
||||
"loadTasks": "Aufgaben konnten nicht geladen werden",
|
||||
|
|
@ -104,5 +106,123 @@
|
|||
"taskCompleted": "Aufgabe erledigt",
|
||||
"projectCreated": "Projekt erstellt",
|
||||
"labelCreated": "Label erstellt"
|
||||
},
|
||||
"taskForm": {
|
||||
"titlePlaceholder": "Aufgabentitel...",
|
||||
"addDescription": "Beschreibung hinzufügen...",
|
||||
"description": "Beschreibung",
|
||||
"scheduling": "Zeitplanung",
|
||||
"due": "Fällig",
|
||||
"dueDate": "Fälligkeit",
|
||||
"time": "Uhrzeit",
|
||||
"start": "Start",
|
||||
"startDate": "Startdatum",
|
||||
"status": "Status",
|
||||
"tags": "Tags",
|
||||
"subtasks": "Subtasks",
|
||||
"recurrence": "Wiederholung",
|
||||
"assignee": "Zuständig",
|
||||
"involved": "Beteiligte",
|
||||
"assignPlaceholder": "Zuweisen...",
|
||||
"assignLabel": "Zuweisen",
|
||||
"assignPerson": "Person zuweisen...",
|
||||
"addPeoplePlaceholder": "Personen hinzufügen...",
|
||||
"addPersonLabel": "Person hinzufügen",
|
||||
"addPlaceholder": "Hinzufügen...",
|
||||
"addLabel": "Hinzufügen",
|
||||
"nameOrEmail": "Name oder E-Mail...",
|
||||
"storypoints": "Storypoints",
|
||||
"duration": "Dauer",
|
||||
"fun": "Spaß",
|
||||
"links": "Verknüpfungen",
|
||||
"addLink": "+ Verknüpfen",
|
||||
"confirmDelete": "Wirklich löschen?",
|
||||
"yesDelete": "Ja, löschen",
|
||||
"openDetails": "Details öffnen",
|
||||
"clickForCreatedDate": "Klicken für Erstellungsdatum",
|
||||
"created": "Erstellt",
|
||||
"completed": "Erledigt"
|
||||
},
|
||||
"filters": {
|
||||
"clearFilter": "Filter löschen",
|
||||
"manageTags": "Tags verwalten",
|
||||
"hideCompleted": "Erledigte ausblenden",
|
||||
"showCompleted": "Erledigte anzeigen",
|
||||
"searchTasks": "Aufgaben suchen...",
|
||||
"resetFilters": "Zurücksetzen",
|
||||
"select": "Auswählen",
|
||||
"noTagsAvailable": "Keine Tags vorhanden",
|
||||
"date": "Datum",
|
||||
"priorityShort": "Priorit.",
|
||||
"name": "Name",
|
||||
"sortByDueDate": "Nach Fälligkeitsdatum sortieren",
|
||||
"sortByPriority": "Nach Priorität sortieren",
|
||||
"sortAlphabetical": "Alphabetisch sortieren",
|
||||
"resetAll": "Filter zurücksetzen"
|
||||
},
|
||||
"tags": {
|
||||
"allTags": "Alle Tags",
|
||||
"showAllTags": "Alle Tags anzeigen",
|
||||
"noTagsAvailable": "Keine Tags vorhanden",
|
||||
"createTag": "Tag erstellen",
|
||||
"newTag": "Neuer Tag",
|
||||
"editTag": "Tag bearbeiten",
|
||||
"deleteTag": "Tag löschen",
|
||||
"tagName": "Tag Name",
|
||||
"searchTags": "Tags suchen...",
|
||||
"clearSearch": "Suche leeren",
|
||||
"createShort": "+ Erstellen",
|
||||
"noTagsFound": "Keine Tags gefunden für \"{query}\"",
|
||||
"selectTags": "Tags auswählen...",
|
||||
"tagsLabel": "Tags:",
|
||||
"filterLabel": "Filter:",
|
||||
"sortBy": "Nach {field} sortieren"
|
||||
},
|
||||
"subtasks": {
|
||||
"dragToSort": "Ziehen zum Sortieren",
|
||||
"addPlaceholder": "Subtask hinzufügen...",
|
||||
"add": "Hinzufügen"
|
||||
},
|
||||
"reminders": {
|
||||
"label": "Erinnerung",
|
||||
"none": "Keine",
|
||||
"atTime": "Zur Fälligkeitszeit",
|
||||
"5min": "5 Minuten vorher",
|
||||
"15min": "15 Minuten vorher",
|
||||
"30min": "30 Minuten vorher",
|
||||
"1hour": "1 Stunde vorher",
|
||||
"2hours": "2 Stunden vorher",
|
||||
"1day": "1 Tag vorher",
|
||||
"2days": "2 Tage vorher",
|
||||
"1week": "1 Woche vorher",
|
||||
"added": "Erinnerung hinzugefügt",
|
||||
"removed": "Erinnerung entfernt",
|
||||
"requiresDueDate": "Erinnerungen brauchen ein Fälligkeitsdatum",
|
||||
"type": "Typ",
|
||||
"push": "Push",
|
||||
"email": "E-Mail",
|
||||
"both": "Push & E-Mail"
|
||||
},
|
||||
"durationPicker": {
|
||||
"minutes": "Minuten",
|
||||
"hours": "Stunden",
|
||||
"days": "Tage",
|
||||
"value": "Wert"
|
||||
},
|
||||
"kanban": {
|
||||
"newTask": "Neue Aufgabe...",
|
||||
"addTask": "Aufgabe hinzufügen",
|
||||
"add": "Hinzufügen",
|
||||
"edit": "Bearbeiten",
|
||||
"restore": "Wiederherstellen",
|
||||
"complete": "Erledigen",
|
||||
"deleteTitle": "Aufgabe löschen?",
|
||||
"deleteMessage": "Diese Aufgabe wird unwiderruflich gelöscht.",
|
||||
"assignedTo": "Zuständig: {name}",
|
||||
"involvedContact": "Beteiligt: {name}"
|
||||
},
|
||||
"toolbar": {
|
||||
"taskOptions": "Aufgaben-Optionen",
|
||||
"switchView": "Ansicht wechseln"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,9 @@
|
|||
"error": "Error",
|
||||
"success": "Success",
|
||||
"loading": "Loading...",
|
||||
"noResults": "No results"
|
||||
"noResults": "No results",
|
||||
"reset": "Reset",
|
||||
"filter": "Filter"
|
||||
},
|
||||
"errors": {
|
||||
"loadTasks": "Failed to load tasks",
|
||||
|
|
@ -104,5 +106,123 @@
|
|||
"taskCompleted": "Task completed",
|
||||
"projectCreated": "Project created",
|
||||
"labelCreated": "Label created"
|
||||
},
|
||||
"taskForm": {
|
||||
"titlePlaceholder": "Task title...",
|
||||
"addDescription": "Add description...",
|
||||
"description": "Description",
|
||||
"scheduling": "Scheduling",
|
||||
"due": "Due",
|
||||
"dueDate": "Due date",
|
||||
"time": "Time",
|
||||
"start": "Start",
|
||||
"startDate": "Start date",
|
||||
"status": "Status",
|
||||
"tags": "Tags",
|
||||
"subtasks": "Subtasks",
|
||||
"recurrence": "Recurrence",
|
||||
"assignee": "Assignee",
|
||||
"involved": "Involved",
|
||||
"assignPlaceholder": "Assign...",
|
||||
"assignLabel": "Assign",
|
||||
"assignPerson": "Assign person...",
|
||||
"addPeoplePlaceholder": "Add people...",
|
||||
"addPersonLabel": "Add person",
|
||||
"addPlaceholder": "Add...",
|
||||
"addLabel": "Add",
|
||||
"nameOrEmail": "Name or email...",
|
||||
"storypoints": "Story points",
|
||||
"duration": "Duration",
|
||||
"fun": "Fun",
|
||||
"links": "Links",
|
||||
"addLink": "+ Link",
|
||||
"confirmDelete": "Really delete?",
|
||||
"yesDelete": "Yes, delete",
|
||||
"openDetails": "Open details",
|
||||
"clickForCreatedDate": "Click for created date",
|
||||
"created": "Created",
|
||||
"completed": "Completed"
|
||||
},
|
||||
"filters": {
|
||||
"clearFilter": "Clear filter",
|
||||
"manageTags": "Manage tags",
|
||||
"hideCompleted": "Hide completed",
|
||||
"showCompleted": "Show completed",
|
||||
"searchTasks": "Search tasks...",
|
||||
"resetFilters": "Reset",
|
||||
"select": "Select",
|
||||
"noTagsAvailable": "No tags available",
|
||||
"date": "Date",
|
||||
"priorityShort": "Priority",
|
||||
"name": "Name",
|
||||
"sortByDueDate": "Sort by due date",
|
||||
"sortByPriority": "Sort by priority",
|
||||
"sortAlphabetical": "Sort alphabetically",
|
||||
"resetAll": "Reset filters"
|
||||
},
|
||||
"tags": {
|
||||
"allTags": "All tags",
|
||||
"showAllTags": "Show all tags",
|
||||
"noTagsAvailable": "No tags available",
|
||||
"createTag": "Create tag",
|
||||
"newTag": "New tag",
|
||||
"editTag": "Edit tag",
|
||||
"deleteTag": "Delete tag",
|
||||
"tagName": "Tag name",
|
||||
"searchTags": "Search tags...",
|
||||
"clearSearch": "Clear search",
|
||||
"createShort": "+ Create",
|
||||
"noTagsFound": "No tags found for \"{query}\"",
|
||||
"selectTags": "Select tags...",
|
||||
"tagsLabel": "Tags:",
|
||||
"filterLabel": "Filter:",
|
||||
"sortBy": "Sort by {field}"
|
||||
},
|
||||
"subtasks": {
|
||||
"dragToSort": "Drag to sort",
|
||||
"addPlaceholder": "Add subtask...",
|
||||
"add": "Add"
|
||||
},
|
||||
"reminders": {
|
||||
"label": "Reminder",
|
||||
"none": "None",
|
||||
"atTime": "At time of task",
|
||||
"5min": "5 minutes before",
|
||||
"15min": "15 minutes before",
|
||||
"30min": "30 minutes before",
|
||||
"1hour": "1 hour before",
|
||||
"2hours": "2 hours before",
|
||||
"1day": "1 day before",
|
||||
"2days": "2 days before",
|
||||
"1week": "1 week before",
|
||||
"added": "Reminder added",
|
||||
"removed": "Reminder removed",
|
||||
"requiresDueDate": "Reminders require a due date",
|
||||
"type": "Type",
|
||||
"push": "Push",
|
||||
"email": "Email",
|
||||
"both": "Push & Email"
|
||||
},
|
||||
"durationPicker": {
|
||||
"minutes": "Minutes",
|
||||
"hours": "Hours",
|
||||
"days": "Days",
|
||||
"value": "Value"
|
||||
},
|
||||
"kanban": {
|
||||
"newTask": "New task...",
|
||||
"addTask": "Add task",
|
||||
"add": "Add",
|
||||
"edit": "Edit",
|
||||
"restore": "Restore",
|
||||
"complete": "Complete",
|
||||
"deleteTitle": "Delete task?",
|
||||
"deleteMessage": "This task will be permanently deleted.",
|
||||
"assignedTo": "Assigned to: {name}",
|
||||
"involvedContact": "Involved: {name}"
|
||||
},
|
||||
"toolbar": {
|
||||
"taskOptions": "Task options",
|
||||
"switchView": "Switch view"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,9 @@
|
|||
"error": "Error",
|
||||
"success": "Éxito",
|
||||
"loading": "Cargando...",
|
||||
"noResults": "Sin resultados"
|
||||
"noResults": "Sin resultados",
|
||||
"reset": "Restablecer",
|
||||
"filter": "Filtro"
|
||||
},
|
||||
"errors": {
|
||||
"loadTasks": "No se pudieron cargar las tareas",
|
||||
|
|
@ -104,5 +106,123 @@
|
|||
"taskCompleted": "Tarea completada",
|
||||
"projectCreated": "Proyecto creado",
|
||||
"labelCreated": "Etiqueta creada"
|
||||
},
|
||||
"taskForm": {
|
||||
"titlePlaceholder": "Título de la tarea...",
|
||||
"addDescription": "Añadir descripción...",
|
||||
"description": "Descripción",
|
||||
"scheduling": "Programación",
|
||||
"due": "Vencimiento",
|
||||
"dueDate": "Fecha de vencimiento",
|
||||
"time": "Hora",
|
||||
"start": "Inicio",
|
||||
"startDate": "Fecha de inicio",
|
||||
"status": "Estado",
|
||||
"tags": "Tags",
|
||||
"subtasks": "Subtareas",
|
||||
"recurrence": "Recurrencia",
|
||||
"assignee": "Responsable",
|
||||
"involved": "Participantes",
|
||||
"assignPlaceholder": "Asignar...",
|
||||
"assignLabel": "Asignar",
|
||||
"assignPerson": "Asignar persona...",
|
||||
"addPeoplePlaceholder": "Añadir personas...",
|
||||
"addPersonLabel": "Añadir persona",
|
||||
"addPlaceholder": "Añadir...",
|
||||
"addLabel": "Añadir",
|
||||
"nameOrEmail": "Nombre o correo...",
|
||||
"storypoints": "Story points",
|
||||
"duration": "Duración",
|
||||
"fun": "Diversión",
|
||||
"links": "Enlaces",
|
||||
"addLink": "+ Enlazar",
|
||||
"confirmDelete": "¿Eliminar realmente?",
|
||||
"yesDelete": "Sí, eliminar",
|
||||
"openDetails": "Abrir detalles",
|
||||
"clickForCreatedDate": "Clic para fecha de creación",
|
||||
"created": "Creado",
|
||||
"completed": "Completado"
|
||||
},
|
||||
"filters": {
|
||||
"clearFilter": "Borrar filtro",
|
||||
"manageTags": "Gestionar tags",
|
||||
"hideCompleted": "Ocultar completadas",
|
||||
"showCompleted": "Mostrar completadas",
|
||||
"searchTasks": "Buscar tareas...",
|
||||
"resetFilters": "Restablecer",
|
||||
"select": "Seleccionar",
|
||||
"noTagsAvailable": "No hay tags disponibles",
|
||||
"date": "Fecha",
|
||||
"priorityShort": "Prioridad",
|
||||
"name": "Nombre",
|
||||
"sortByDueDate": "Ordenar por fecha de vencimiento",
|
||||
"sortByPriority": "Ordenar por prioridad",
|
||||
"sortAlphabetical": "Ordenar alfabéticamente",
|
||||
"resetAll": "Restablecer filtros"
|
||||
},
|
||||
"tags": {
|
||||
"allTags": "Todos los tags",
|
||||
"showAllTags": "Mostrar todos los tags",
|
||||
"noTagsAvailable": "No hay tags disponibles",
|
||||
"createTag": "Crear tag",
|
||||
"newTag": "Nuevo tag",
|
||||
"editTag": "Editar tag",
|
||||
"deleteTag": "Eliminar tag",
|
||||
"tagName": "Nombre del tag",
|
||||
"searchTags": "Buscar tags...",
|
||||
"clearSearch": "Borrar búsqueda",
|
||||
"createShort": "+ Crear",
|
||||
"noTagsFound": "No se encontraron tags para \"{query}\"",
|
||||
"selectTags": "Seleccionar tags...",
|
||||
"tagsLabel": "Tags:",
|
||||
"filterLabel": "Filtro:",
|
||||
"sortBy": "Ordenar por {field}"
|
||||
},
|
||||
"subtasks": {
|
||||
"dragToSort": "Arrastrar para ordenar",
|
||||
"addPlaceholder": "Añadir subtarea...",
|
||||
"add": "Añadir"
|
||||
},
|
||||
"reminders": {
|
||||
"label": "Recordatorio",
|
||||
"none": "Ninguno",
|
||||
"atTime": "A la hora de la tarea",
|
||||
"5min": "5 minutos antes",
|
||||
"15min": "15 minutos antes",
|
||||
"30min": "30 minutos antes",
|
||||
"1hour": "1 hora antes",
|
||||
"2hours": "2 horas antes",
|
||||
"1day": "1 día antes",
|
||||
"2days": "2 días antes",
|
||||
"1week": "1 semana antes",
|
||||
"added": "Recordatorio añadido",
|
||||
"removed": "Recordatorio eliminado",
|
||||
"requiresDueDate": "Los recordatorios requieren una fecha de vencimiento",
|
||||
"type": "Tipo",
|
||||
"push": "Push",
|
||||
"email": "Correo",
|
||||
"both": "Push y Correo"
|
||||
},
|
||||
"durationPicker": {
|
||||
"minutes": "Minutos",
|
||||
"hours": "Horas",
|
||||
"days": "Días",
|
||||
"value": "Valor"
|
||||
},
|
||||
"kanban": {
|
||||
"newTask": "Nueva tarea...",
|
||||
"addTask": "Añadir tarea",
|
||||
"add": "Añadir",
|
||||
"edit": "Editar",
|
||||
"restore": "Restaurar",
|
||||
"complete": "Completar",
|
||||
"deleteTitle": "¿Eliminar tarea?",
|
||||
"deleteMessage": "Esta tarea se eliminará permanentemente.",
|
||||
"assignedTo": "Responsable: {name}",
|
||||
"involvedContact": "Participante: {name}"
|
||||
},
|
||||
"toolbar": {
|
||||
"taskOptions": "Opciones de tareas",
|
||||
"switchView": "Cambiar vista"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,9 @@
|
|||
"error": "Erreur",
|
||||
"success": "Succès",
|
||||
"loading": "Chargement...",
|
||||
"noResults": "Aucun résultat"
|
||||
"noResults": "Aucun résultat",
|
||||
"reset": "Réinitialiser",
|
||||
"filter": "Filtre"
|
||||
},
|
||||
"errors": {
|
||||
"loadTasks": "Impossible de charger les tâches",
|
||||
|
|
@ -104,5 +106,123 @@
|
|||
"taskCompleted": "Tâche terminée",
|
||||
"projectCreated": "Projet créé",
|
||||
"labelCreated": "Label créé"
|
||||
},
|
||||
"taskForm": {
|
||||
"titlePlaceholder": "Titre de la tâche...",
|
||||
"addDescription": "Ajouter une description...",
|
||||
"description": "Description",
|
||||
"scheduling": "Planification",
|
||||
"due": "Échéance",
|
||||
"dueDate": "Date d'échéance",
|
||||
"time": "Heure",
|
||||
"start": "Début",
|
||||
"startDate": "Date de début",
|
||||
"status": "Statut",
|
||||
"tags": "Tags",
|
||||
"subtasks": "Sous-tâches",
|
||||
"recurrence": "Récurrence",
|
||||
"assignee": "Responsable",
|
||||
"involved": "Participants",
|
||||
"assignPlaceholder": "Assigner...",
|
||||
"assignLabel": "Assigner",
|
||||
"assignPerson": "Assigner une personne...",
|
||||
"addPeoplePlaceholder": "Ajouter des personnes...",
|
||||
"addPersonLabel": "Ajouter une personne",
|
||||
"addPlaceholder": "Ajouter...",
|
||||
"addLabel": "Ajouter",
|
||||
"nameOrEmail": "Nom ou e-mail...",
|
||||
"storypoints": "Story points",
|
||||
"duration": "Durée",
|
||||
"fun": "Plaisir",
|
||||
"links": "Liens",
|
||||
"addLink": "+ Lier",
|
||||
"confirmDelete": "Vraiment supprimer ?",
|
||||
"yesDelete": "Oui, supprimer",
|
||||
"openDetails": "Ouvrir les détails",
|
||||
"clickForCreatedDate": "Cliquer pour la date de création",
|
||||
"created": "Créé",
|
||||
"completed": "Terminé"
|
||||
},
|
||||
"filters": {
|
||||
"clearFilter": "Effacer le filtre",
|
||||
"manageTags": "Gérer les tags",
|
||||
"hideCompleted": "Masquer les terminées",
|
||||
"showCompleted": "Afficher les terminées",
|
||||
"searchTasks": "Rechercher des tâches...",
|
||||
"resetFilters": "Réinitialiser",
|
||||
"select": "Sélectionner",
|
||||
"noTagsAvailable": "Aucun tag disponible",
|
||||
"date": "Date",
|
||||
"priorityShort": "Priorité",
|
||||
"name": "Nom",
|
||||
"sortByDueDate": "Trier par date d'échéance",
|
||||
"sortByPriority": "Trier par priorité",
|
||||
"sortAlphabetical": "Trier par ordre alphabétique",
|
||||
"resetAll": "Réinitialiser les filtres"
|
||||
},
|
||||
"tags": {
|
||||
"allTags": "Tous les tags",
|
||||
"showAllTags": "Afficher tous les tags",
|
||||
"noTagsAvailable": "Aucun tag disponible",
|
||||
"createTag": "Créer un tag",
|
||||
"newTag": "Nouveau tag",
|
||||
"editTag": "Modifier le tag",
|
||||
"deleteTag": "Supprimer le tag",
|
||||
"tagName": "Nom du tag",
|
||||
"searchTags": "Rechercher des tags...",
|
||||
"clearSearch": "Effacer la recherche",
|
||||
"createShort": "+ Créer",
|
||||
"noTagsFound": "Aucun tag trouvé pour \"{query}\"",
|
||||
"selectTags": "Sélectionner des tags...",
|
||||
"tagsLabel": "Tags :",
|
||||
"filterLabel": "Filtre :",
|
||||
"sortBy": "Trier par {field}"
|
||||
},
|
||||
"subtasks": {
|
||||
"dragToSort": "Glisser pour trier",
|
||||
"addPlaceholder": "Ajouter une sous-tâche...",
|
||||
"add": "Ajouter"
|
||||
},
|
||||
"reminders": {
|
||||
"label": "Rappel",
|
||||
"none": "Aucun",
|
||||
"atTime": "À l'heure de la tâche",
|
||||
"5min": "5 minutes avant",
|
||||
"15min": "15 minutes avant",
|
||||
"30min": "30 minutes avant",
|
||||
"1hour": "1 heure avant",
|
||||
"2hours": "2 heures avant",
|
||||
"1day": "1 jour avant",
|
||||
"2days": "2 jours avant",
|
||||
"1week": "1 semaine avant",
|
||||
"added": "Rappel ajouté",
|
||||
"removed": "Rappel supprimé",
|
||||
"requiresDueDate": "Les rappels nécessitent une date d'échéance",
|
||||
"type": "Type",
|
||||
"push": "Push",
|
||||
"email": "E-mail",
|
||||
"both": "Push & E-mail"
|
||||
},
|
||||
"durationPicker": {
|
||||
"minutes": "Minutes",
|
||||
"hours": "Heures",
|
||||
"days": "Jours",
|
||||
"value": "Valeur"
|
||||
},
|
||||
"kanban": {
|
||||
"newTask": "Nouvelle tâche...",
|
||||
"addTask": "Ajouter une tâche",
|
||||
"add": "Ajouter",
|
||||
"edit": "Modifier",
|
||||
"restore": "Restaurer",
|
||||
"complete": "Terminer",
|
||||
"deleteTitle": "Supprimer la tâche ?",
|
||||
"deleteMessage": "Cette tâche sera supprimée définitivement.",
|
||||
"assignedTo": "Responsable : {name}",
|
||||
"involvedContact": "Participant : {name}"
|
||||
},
|
||||
"toolbar": {
|
||||
"taskOptions": "Options des tâches",
|
||||
"switchView": "Changer de vue"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,9 @@
|
|||
"error": "Errore",
|
||||
"success": "Successo",
|
||||
"loading": "Caricamento...",
|
||||
"noResults": "Nessun risultato"
|
||||
"noResults": "Nessun risultato",
|
||||
"reset": "Reimposta",
|
||||
"filter": "Filtro"
|
||||
},
|
||||
"errors": {
|
||||
"loadTasks": "Impossibile caricare le attività",
|
||||
|
|
@ -104,5 +106,123 @@
|
|||
"taskCompleted": "Attività completata",
|
||||
"projectCreated": "Progetto creato",
|
||||
"labelCreated": "Etichetta creata"
|
||||
},
|
||||
"taskForm": {
|
||||
"titlePlaceholder": "Titolo dell'attività...",
|
||||
"addDescription": "Aggiungi descrizione...",
|
||||
"description": "Descrizione",
|
||||
"scheduling": "Pianificazione",
|
||||
"due": "Scadenza",
|
||||
"dueDate": "Data di scadenza",
|
||||
"time": "Orario",
|
||||
"start": "Inizio",
|
||||
"startDate": "Data di inizio",
|
||||
"status": "Stato",
|
||||
"tags": "Tags",
|
||||
"subtasks": "Sotto-attività",
|
||||
"recurrence": "Ricorrenza",
|
||||
"assignee": "Responsabile",
|
||||
"involved": "Partecipanti",
|
||||
"assignPlaceholder": "Assegna...",
|
||||
"assignLabel": "Assegna",
|
||||
"assignPerson": "Assegna persona...",
|
||||
"addPeoplePlaceholder": "Aggiungi persone...",
|
||||
"addPersonLabel": "Aggiungi persona",
|
||||
"addPlaceholder": "Aggiungi...",
|
||||
"addLabel": "Aggiungi",
|
||||
"nameOrEmail": "Nome o e-mail...",
|
||||
"storypoints": "Story points",
|
||||
"duration": "Durata",
|
||||
"fun": "Divertimento",
|
||||
"links": "Collegamento",
|
||||
"addLink": "+ Collega",
|
||||
"confirmDelete": "Eliminare davvero?",
|
||||
"yesDelete": "Sì, elimina",
|
||||
"openDetails": "Apri dettagli",
|
||||
"clickForCreatedDate": "Clicca per data di creazione",
|
||||
"created": "Creato",
|
||||
"completed": "Completato"
|
||||
},
|
||||
"filters": {
|
||||
"clearFilter": "Cancella filtro",
|
||||
"manageTags": "Gestisci tags",
|
||||
"hideCompleted": "Nascondi completate",
|
||||
"showCompleted": "Mostra completate",
|
||||
"searchTasks": "Cerca attività...",
|
||||
"resetFilters": "Reimposta",
|
||||
"select": "Seleziona",
|
||||
"noTagsAvailable": "Nessun tag disponibile",
|
||||
"date": "Data",
|
||||
"priorityShort": "Priorità",
|
||||
"name": "Nome",
|
||||
"sortByDueDate": "Ordina per scadenza",
|
||||
"sortByPriority": "Ordina per priorità",
|
||||
"sortAlphabetical": "Ordina alfabeticamente",
|
||||
"resetAll": "Reimposta filtri"
|
||||
},
|
||||
"tags": {
|
||||
"allTags": "Tutti i tags",
|
||||
"showAllTags": "Mostra tutti i tags",
|
||||
"noTagsAvailable": "Nessun tag disponibile",
|
||||
"createTag": "Crea tag",
|
||||
"newTag": "Nuovo tag",
|
||||
"editTag": "Modifica tag",
|
||||
"deleteTag": "Elimina tag",
|
||||
"tagName": "Nome del tag",
|
||||
"searchTags": "Cerca tags...",
|
||||
"clearSearch": "Cancella ricerca",
|
||||
"createShort": "+ Crea",
|
||||
"noTagsFound": "Nessun tag trovato per \"{query}\"",
|
||||
"selectTags": "Seleziona tags...",
|
||||
"tagsLabel": "Tags:",
|
||||
"filterLabel": "Filtro:",
|
||||
"sortBy": "Ordina per {field}"
|
||||
},
|
||||
"subtasks": {
|
||||
"dragToSort": "Trascina per ordinare",
|
||||
"addPlaceholder": "Aggiungi sotto-attività...",
|
||||
"add": "Aggiungi"
|
||||
},
|
||||
"reminders": {
|
||||
"label": "Promemoria",
|
||||
"none": "Nessuno",
|
||||
"atTime": "All'ora dell'attività",
|
||||
"5min": "5 minuti prima",
|
||||
"15min": "15 minuti prima",
|
||||
"30min": "30 minuti prima",
|
||||
"1hour": "1 ora prima",
|
||||
"2hours": "2 ore prima",
|
||||
"1day": "1 giorno prima",
|
||||
"2days": "2 giorni prima",
|
||||
"1week": "1 settimana prima",
|
||||
"added": "Promemoria aggiunto",
|
||||
"removed": "Promemoria rimosso",
|
||||
"requiresDueDate": "I promemoria richiedono una data di scadenza",
|
||||
"type": "Tipo",
|
||||
"push": "Push",
|
||||
"email": "E-mail",
|
||||
"both": "Push ed E-mail"
|
||||
},
|
||||
"durationPicker": {
|
||||
"minutes": "Minuti",
|
||||
"hours": "Ore",
|
||||
"days": "Giorni",
|
||||
"value": "Valore"
|
||||
},
|
||||
"kanban": {
|
||||
"newTask": "Nuova attività...",
|
||||
"addTask": "Aggiungi attività",
|
||||
"add": "Aggiungi",
|
||||
"edit": "Modifica",
|
||||
"restore": "Ripristina",
|
||||
"complete": "Completa",
|
||||
"deleteTitle": "Eliminare attività?",
|
||||
"deleteMessage": "Questa attività verrà eliminata definitivamente.",
|
||||
"assignedTo": "Responsabile: {name}",
|
||||
"involvedContact": "Partecipante: {name}"
|
||||
},
|
||||
"toolbar": {
|
||||
"taskOptions": "Opzioni attività",
|
||||
"switchView": "Cambia vista"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue