feat: enhance DND nesting & promotion, isolated tags API, fix completion bounce UI, add Admin i18n switcher, refine typography & polish UX (v0.7.2)
Build and Push Docker Image / build-and-push (push) Successful in 9m59s
Build and Push Docker Image / build-and-push (push) Successful in 9m59s
This commit is contained in:
@@ -15,6 +15,11 @@
|
||||
- `src/middleware.ts`: Route-level auth and admin role verification guard.
|
||||
|
||||
- **Completed So Far:**
|
||||
- Standardized user-isolated Tag database REST API (`/api/tags`) ensuring private custom tags per account, preventing cross-user data leakage and providing live tag autocomplete (v0.7.2).
|
||||
- Streamlined Task Drag-and-Drop (DND) interaction model: hovering over a task nests as a subtask, dropping on blank list background elevates to root level, and prevents duplicate/looping trees (v0.7.2).
|
||||
- Fixed completed task toggle button layout shift and centered list loader animations (v0.7.2).
|
||||
- Constrained inline title edit double-click trigger zone strictly to task title text span (v0.7.2).
|
||||
- Integrated i18n localization with language switcher in Admin console (`/admin`), and upgraded placeholders/dialogs across EN/KO/JA to polite, natural phrasing (v0.7.2).
|
||||
- Standardized complete localization across all 3 supported languages (EN, KO, JA) without partial fallback leaks; translated all Settings Modal tabs (Profile, Preferences, Labs, Sync Integrations, Admin) and interactive guides (v0.7.1).
|
||||
- Resolved list header rename layout shift bug where editing list title pushed the completed items toggle button (v0.7.1).
|
||||
- Enhanced Markdown Note Editor preview/editor canvas alignment and unified description typography heights (v0.7.1).
|
||||
|
||||
@@ -85,7 +85,8 @@ src/
|
||||
| `checkpoint-v0.6.3` | 노션 스타일 6도트 드래그 핸들(`⋮⋮`) & DND 태스크 재정렬, F2 단축키 인라인 제목 수정, 우측 호버 서브태스크 추가 버튼, 6초 플로팅 인터랙티브 실행 취소(Undo) 토스트 배너 탑재, 휴지통 및 로컬 스토리지 트리 중복 버그 수정 | 2026-08-21 |
|
||||
| `checkpoint-v0.6.4` | i18n 모듈러 사전 아키텍처(`types.ts`, `locales/en.ts`, `locales/ko.ts`, `locales/ja.ts`) 분리 및 번역 완성도 100%, 어드민 콘솔(`/admin`) 데모 프리뷰 및 멀티유저 게이트 연동, 설정 모달 Admin 탭 정보 고도화, 사이드바 프로필 메뉴 내 Admin Console 진입점 추가 | 2026-08-21 |
|
||||
| `checkpoint-v0.7.0` | 좌측 사이드바 UI/UX 전면 리디자인(브랜드 로고 SVG, 중앙정렬 매트 검색창, 바닥 밀착 프로필), 뷰 전환(리스트/칸반) 미니멀 원터치 토글 및 실험실 연동 버그 수정, 휴지통 카운트 실시간 동기화 & 기본화면 튕김 버그 수정, 첫 가입 계정 자동 ADMIN 승격 및 실시간 Admin API(/api/admin/users, /api/admin/stats) 구축, 서브태스크 인라인 추가 및 체크박스 Optimistic UI 실시간 렌더링, 메모장 기본 Preview 모드 & 원터치 토글, N-depth 계층형 DND 드래그 앤 드롭 재정렬 및 재귀 하위 태스크 캐스케이드 삭제 지원 | 2026-08-21 |
|
||||
| `checkpoint-v0.7.1` | **(현재 최신)** 설정 모달(5개 탭 및 가이드/프리뷰 전면) i18n 3개국어(EN/KO/JA) 100% 완전 번역 및 언어 로컬스토리지 영구 기억/자동 감지 적용, 태스크 목록 제목 인라인 수정 시 완료항목 토글 버튼 밀림 flexbox 레이아웃 버그 수정, 메모장 프리뷰/에디터 높이 및 플레이스홀더 타이포그래피 일관성 보정, 우측 상세 패널 우선순위 브라우저 기본 드롭다운을 중앙 퀵애드와 동일한 세련된 컬러 팝오버 칩 UI로 통일 | 2026-08-22 |
|
||||
| `checkpoint-v0.7.1` | 설정 모달(5개 탭 및 가이드/프리뷰 전면) i18n 3개국어(EN/KO/JA) 100% 완전 번역 및 언어 로컬스토리지 영구 기억/자동 감지 적용, 태스크 목록 제목 인라인 수정 시 완료항목 토글 버튼 밀림 flexbox 레이아웃 버그 수정, 메모장 프리뷰/에디터 높이 및 플레이스홀더 타이포그래피 일관성 보정, 우측 상세 패널 우선순위 브라우저 기본 드롭다운을 중앙 퀵애드와 동일한 세련된 컬러 팝오버 칩 UI로 통일 | 2026-08-22 |
|
||||
| `checkpoint-v0.7.2` | **(현재 최신)** 태그(Tag) 사용자 계정별 데이터베이스 격리 API 구축(/api/tags), DND 드래그앤드롭 상위 태스크 하위 편입 단일화 및 빈 배경 드롭 1단계 승격 지원, 완료항목 토글 시 로딩 UI 밀림 버그 수정, 인라인 제목 수정 더블클릭 영역 텍스트 한정, 어드민 콘솔 다국어(i18n) 언어 선택기 지원, 친절하고 간결한 톤앤매너 플레이스홀더 전면 적용 | 2026-08-22 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import Link from "next/link";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { getDemoStore } from "@/lib/mockData";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import { LanguageSelector } from "@/components/ui/LanguageSelector";
|
||||
|
||||
interface AdminUser {
|
||||
id: string;
|
||||
@@ -47,7 +48,7 @@ const INITIAL_DEMO_USERS: AdminUser[] = [
|
||||
|
||||
export default function AdminPage() {
|
||||
const { data: session, status } = useSession();
|
||||
const { t } = useI18n();
|
||||
const { t, lang } = useI18n();
|
||||
|
||||
const isDemo = status === "unauthenticated" || !session;
|
||||
|
||||
@@ -232,18 +233,19 @@ export default function AdminPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<LanguageSelector />
|
||||
{isDemo ? (
|
||||
<Link href="/demo" className="btn btn-ghost btn-sm">
|
||||
← Back to Demo
|
||||
← {lang === "ko" ? "데모로 돌아가기" : lang === "ja" ? "デモに戻る" : "Back to Demo"}
|
||||
</Link>
|
||||
) : (
|
||||
<button type="button" className="btn btn-ghost btn-sm" onClick={fetchAdminData}>
|
||||
🔄 Refresh Data
|
||||
🔄 {lang === "ko" ? "새로고침" : lang === "ja" ? "更新" : "Refresh"}
|
||||
</button>
|
||||
)}
|
||||
<Link href="/" className="btn btn-primary btn-sm">
|
||||
🚀 Open Dashboard
|
||||
🚀 {lang === "ko" ? "할 일 대시보드" : lang === "ja" ? "タスク一覧" : "Open Dashboard"}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TaskList, Task, List, User } from "../tasks/TaskList";
|
||||
import { useUserPrefs, usePrefsStyle } from "@/lib/useUserPrefs";
|
||||
import { TaskDetail } from "../tasks/TaskDetail";
|
||||
import { CommandPalette } from "../ui/CommandPalette";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import {
|
||||
getDemoStore,
|
||||
saveDemoStore,
|
||||
@@ -58,6 +59,7 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
const [showCompleted, setShowCompleted] = useState(false);
|
||||
const [cmdPaletteOpen, setCmdPaletteOpen] = useState(false);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const { t } = useI18n();
|
||||
|
||||
// Global user preferences (reactive)
|
||||
const { prefs, updatePrefs } = useUserPrefs();
|
||||
@@ -677,12 +679,12 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
<div className="undo-toast-content">
|
||||
<span className="undo-toast-icon">🗑️</span>
|
||||
<span className="undo-toast-msg">
|
||||
<strong>{undoToast.title || "할 일"}</strong>이(가) 삭제되었습니다.
|
||||
<strong>{undoToast.title || t("tasks")}</strong> {t("taskUndoHint")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="undo-toast-actions">
|
||||
<button type="button" className="undo-toast-btn" onClick={handleUndoDelete}>
|
||||
실행 취소
|
||||
{t("undoDelete")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -101,6 +101,27 @@ export function KanbanView({
|
||||
</span>
|
||||
)}
|
||||
|
||||
{task.tags && task.tags.length > 0 && task.tags.map((tg) => {
|
||||
const tagName = typeof tg === "string" ? tg : tg.tag?.name;
|
||||
const tagKey = typeof tg === "string" ? tg : tg.tag?.id || tg.tag?.name;
|
||||
if (!tagName) return null;
|
||||
return (
|
||||
<span
|
||||
key={tagKey}
|
||||
style={{
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
padding: "2px 6px",
|
||||
borderRadius: "var(--radius-sm)",
|
||||
background: "rgba(59, 130, 246, 0.12)",
|
||||
color: "#3B82F6",
|
||||
}}
|
||||
>
|
||||
#{tagName}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
|
||||
{subtaskCount > 0 && (
|
||||
<span
|
||||
style={{
|
||||
|
||||
@@ -40,12 +40,30 @@ export function TaskDetail({
|
||||
const [dueDate, setDueDate] = useState(task.dueDate ? task.dueDate.split("T")[0] : "");
|
||||
const [priority, setPriority] = useState(task.priority);
|
||||
const [tags, setTags] = useState<{ tag: Tag }[]>(task.tags || []);
|
||||
const [allAvailableTags, setAllAvailableTags] = useState<MockTag[]>(() => (typeof window !== "undefined" ? getCustomTags() : []));
|
||||
const [allAvailableTags, setAllAvailableTags] = useState<MockTag[]>(() => (isDemo && typeof window !== "undefined" ? getCustomTags() : []));
|
||||
const [showTagPicker, setShowTagPicker] = useState(false);
|
||||
const [showListPicker, setShowListPicker] = useState(false);
|
||||
const [showPriorityPicker, setShowPriorityPicker] = useState(false);
|
||||
const [newTagName, setNewTagName] = useState("");
|
||||
|
||||
// Load user's actual tags in non-demo mode
|
||||
useEffect(() => {
|
||||
if (isDemo) {
|
||||
if (typeof window !== "undefined") {
|
||||
setAllAvailableTags(getCustomTags());
|
||||
}
|
||||
} else {
|
||||
fetch("/api/tags")
|
||||
.then((res) => (res.ok ? res.json() : []))
|
||||
.then((data) => {
|
||||
if (Array.isArray(data)) {
|
||||
setAllAvailableTags(data);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error("Failed to fetch user tags", err));
|
||||
}
|
||||
}, [isDemo]);
|
||||
|
||||
// Modular Blocks Customization via global prefs
|
||||
const { prefs, updatePrefs } = useUserPrefs();
|
||||
const blockOrder = prefs.detailBlockOrder;
|
||||
@@ -244,22 +262,43 @@ export function TaskDetail({
|
||||
save(task.id, { tags: nextTags });
|
||||
};
|
||||
|
||||
const addCustomTag = () => {
|
||||
const addCustomTag = async () => {
|
||||
const trimmed = newTagName.trim().replace(/^#/, "");
|
||||
if (!trimmed) return;
|
||||
const newTag: MockTag = {
|
||||
id: "tag-" + Date.now(),
|
||||
name: trimmed,
|
||||
color: ["#4B7BF5", "#10B981", "#EF4444", "#F59E0B", "#8B5CF6"][Math.floor(Math.random() * 5)],
|
||||
};
|
||||
const updatedAvailable = [...allAvailableTags, newTag];
|
||||
setAllAvailableTags(updatedAvailable);
|
||||
if (typeof window !== "undefined") {
|
||||
const { saveCustomTags } = require("@/lib/mockData");
|
||||
saveCustomTags(updatedAvailable);
|
||||
const randomColor = ["#4B7BF5", "#10B981", "#EF4444", "#F59E0B", "#8B5CF6"][Math.floor(Math.random() * 5)];
|
||||
|
||||
if (isDemo) {
|
||||
const newTag: MockTag = {
|
||||
id: "tag-" + Date.now(),
|
||||
name: trimmed,
|
||||
color: randomColor,
|
||||
};
|
||||
const updatedAvailable = [...allAvailableTags, newTag];
|
||||
setAllAvailableTags(updatedAvailable);
|
||||
if (typeof window !== "undefined") {
|
||||
const { saveCustomTags } = require("@/lib/mockData");
|
||||
saveCustomTags(updatedAvailable);
|
||||
}
|
||||
toggleTag(newTag);
|
||||
setNewTagName("");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/tags", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: trimmed, color: randomColor }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const createdTag = await res.json();
|
||||
setAllAvailableTags((prev) => [...prev.filter((tItem) => tItem.id !== createdTag.id), createdTag]);
|
||||
toggleTag(createdTag);
|
||||
setNewTagName("");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to create tag", err);
|
||||
}
|
||||
toggleTag(newTag);
|
||||
setNewTagName("");
|
||||
};
|
||||
|
||||
const addSubtask = useCallback(async () => {
|
||||
|
||||
@@ -44,7 +44,7 @@ interface TaskItemProps {
|
||||
isTrashMode?: boolean;
|
||||
onRestore?: (id: string) => void;
|
||||
onPermanentDelete?: (id: string) => void;
|
||||
onDragTask?: (draggedId: string, targetId: string, position: "before" | "after" | "inside") => void;
|
||||
onDragTask?: (draggedId: string, targetId: string, position: "before" | "after" | "inside" | "root") => void;
|
||||
}
|
||||
|
||||
// Recursive Task Tree Item (Supports 1st, 2nd, 3rd, N-level sub-tasks seamlessly)
|
||||
@@ -157,12 +157,12 @@ function RecursiveTaskItem({
|
||||
touchStartX.current = null;
|
||||
};
|
||||
|
||||
const [dragOverPos, setDragOverPos] = useState<"top" | "bottom" | "inside" | null>(null);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
|
||||
return (
|
||||
<div style={{ paddingLeft: depth > 0 ? 24 : 0, position: "relative" }}>
|
||||
<div
|
||||
className={`task-item${task.completed ? " completed" : ""}${isSelected ? " selected" : ""}${dragOverPos ? ` drag-over-${dragOverPos}` : ""}`}
|
||||
className={`task-item${task.completed ? " completed" : ""}${isSelected ? " selected" : ""}${isDragOver ? " drag-over-nested" : ""}`}
|
||||
onClick={() => onSelect(task)}
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
@@ -183,29 +183,25 @@ function RecursiveTaskItem({
|
||||
onDragOver={(e) => {
|
||||
if (isTrashMode) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const relY = e.clientY - rect.top;
|
||||
const h = rect.height;
|
||||
if (relY < h * 0.25) {
|
||||
setDragOverPos("top");
|
||||
} else if (relY > h * 0.75) {
|
||||
setDragOverPos("bottom");
|
||||
} else {
|
||||
setDragOverPos("inside");
|
||||
}
|
||||
if (!isDragOver) setIsDragOver(true);
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
}}
|
||||
onDragLeave={() => setDragOverPos(null)}
|
||||
onDrop={(e) => {
|
||||
if (isTrashMode) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
const draggedId = e.dataTransfer.getData("text/plain");
|
||||
if (draggedId && draggedId !== task.id && onDragTask) {
|
||||
const pos: "before" | "after" | "inside" =
|
||||
dragOverPos === "top" ? "before" : dragOverPos === "bottom" ? "after" : "inside";
|
||||
onDragTask(draggedId, task.id, pos);
|
||||
// Unified interaction: drop on task to nest as subtask
|
||||
onDragTask(draggedId, task.id, "inside");
|
||||
}
|
||||
setDragOverPos(null);
|
||||
}}
|
||||
id={`task-${task.id}`}
|
||||
style={{
|
||||
@@ -213,9 +209,9 @@ function RecursiveTaskItem({
|
||||
marginLeft: depth > 0 ? 8 : 0,
|
||||
transform: `translateX(${swipeOffset}px)`,
|
||||
transition: swipeOffset === 0 ? "transform 0.2s cubic-bezier(0.16, 1, 0.3, 1)" : "none",
|
||||
outline: dragOverPos === "inside" ? "2px dashed var(--accent)" : undefined,
|
||||
outline: isDragOver ? "2px dashed var(--accent)" : undefined,
|
||||
outlineOffset: -2,
|
||||
background: dragOverPos === "inside" ? "var(--accent-light, rgba(75, 123, 245, 0.12))" : undefined,
|
||||
background: isDragOver ? "var(--accent-light, rgba(75, 123, 245, 0.12))" : undefined,
|
||||
}}
|
||||
>
|
||||
{/* Notion-style 6-dot Drag Handle */}
|
||||
@@ -307,17 +303,24 @@ function RecursiveTaskItem({
|
||||
style={{ padding: "2px 6px", fontSize: depth > 0 ? 13 : 14, fontWeight: 500, height: 26, width: "100%" }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="task-title"
|
||||
onDoubleClick={(e) => {
|
||||
if (isTrashMode) return;
|
||||
e.stopPropagation();
|
||||
setEditingTitle(true);
|
||||
}}
|
||||
title="Double click or press F2 to edit"
|
||||
style={{ fontSize: depth > 0 ? 13 : 14, fontWeight: depth === 0 ? 600 : 500 }}
|
||||
>
|
||||
{task.title}
|
||||
<div style={{ display: "inline-flex", alignItems: "center", maxWidth: "100%" }}>
|
||||
<span
|
||||
className="task-title"
|
||||
onDoubleClick={(e) => {
|
||||
if (isTrashMode) return;
|
||||
e.stopPropagation();
|
||||
setEditingTitle(true);
|
||||
}}
|
||||
title="Double click or press F2 to edit"
|
||||
style={{
|
||||
fontSize: depth > 0 ? 13 : 14,
|
||||
fontWeight: depth === 0 ? 600 : 500,
|
||||
cursor: "text",
|
||||
display: "inline-block",
|
||||
}}
|
||||
>
|
||||
{task.title}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -804,9 +807,9 @@ export function TaskList({
|
||||
};
|
||||
|
||||
// Robust Tree-aware reordering function (1st, 2nd, 3rd depth task reordering, promoting to top-level, and demoting into subtask)
|
||||
const handleReorderTasks = useCallback((draggedId: string, targetId: string, position: "before" | "after" | "inside") => {
|
||||
const handleReorderTasks = useCallback((draggedId: string, targetId: string, position: "before" | "after" | "inside" | "root") => {
|
||||
// Prevent dropping onto itself or into its own subtree (which causes loops/disappearing tasks)
|
||||
if (draggedId === targetId) return;
|
||||
if (draggedId === targetId && position !== "root") return;
|
||||
|
||||
setTasks((prev) => {
|
||||
if (isDescendantNode(prev, draggedId, targetId)) {
|
||||
@@ -837,6 +840,25 @@ export function TaskList({
|
||||
|
||||
let determinedParentId: string | null = null;
|
||||
|
||||
if (position === "root") {
|
||||
// Promote directly to top-level (1st depth root task)
|
||||
determinedParentId = null;
|
||||
const nodeAsRoot: Task = { ...safeExtractedNode, parentId: null };
|
||||
const finalTree = [...treeWithoutDragged, nodeAsRoot];
|
||||
|
||||
if (isDemo && typeof window !== "undefined") {
|
||||
const store = getDemoStore();
|
||||
saveDemoStore(store.lists, finalTree as unknown as MockTask[]);
|
||||
} else {
|
||||
fetch(`/api/tasks/${draggedId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ parentId: null }),
|
||||
}).catch((err) => console.error("Failed to promote task to root", err));
|
||||
}
|
||||
return finalTree;
|
||||
}
|
||||
|
||||
// 2. Insert into the target position
|
||||
if (position === "inside") {
|
||||
determinedParentId = targetId;
|
||||
@@ -1100,17 +1122,38 @@ export function TaskList({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
/* Task list container */
|
||||
/* Task list container with background drop support for promoting subtask to 1st depth */
|
||||
<div
|
||||
className="task-list-container"
|
||||
onDragOver={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
}
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
e.preventDefault();
|
||||
const draggedId = e.dataTransfer.getData("text/plain");
|
||||
if (draggedId) {
|
||||
// Promote to 1st depth root task
|
||||
handleReorderTasks(draggedId, "", "root");
|
||||
}
|
||||
}
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading && (
|
||||
<div style={{ padding: "20px", textAlign: "center", color: "var(--text-tertiary)" }}>{t("loading")}</div>
|
||||
{loading && tasks.length === 0 && (
|
||||
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", padding: "40px", color: "var(--text-tertiary)" }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 10 }}>
|
||||
<div style={{ width: 24, height: 24, border: "2px solid var(--border)", borderTopColor: "var(--accent)", borderRadius: "50%", animation: "spin 0.8s linear infinite" }} />
|
||||
<span style={{ fontSize: 13 }}>{t("loading")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!loading && tasks.length === 0 && (
|
||||
<div className="task-list-empty">
|
||||
|
||||
@@ -155,16 +155,16 @@ export const en: TranslationDict = {
|
||||
hideDone: "Hide Done",
|
||||
showDone: "Show Done",
|
||||
completedSection: "Completed",
|
||||
addTaskPlaceholder: "Add a task, press Enter...",
|
||||
addTaskPlaceholder: "What would you like to accomplish?",
|
||||
add: "Add",
|
||||
noTasksYet: "No tasks in this list yet",
|
||||
noTasksYet: "No tasks in this list",
|
||||
selectListToStart: "Select a list to get started",
|
||||
loading: "Loading...",
|
||||
loading: "Loading tasks...",
|
||||
today: "Today",
|
||||
tomorrow: "Tomorrow",
|
||||
|
||||
// Task Detail
|
||||
taskTitlePlaceholder: "Task title...",
|
||||
taskTitlePlaceholder: "Task title",
|
||||
priority: "Priority",
|
||||
priorityNone: "None",
|
||||
priorityLow: "Low",
|
||||
@@ -173,10 +173,10 @@ export const en: TranslationDict = {
|
||||
dueDate: "Due date",
|
||||
clear: "Clear",
|
||||
notes: "Notes",
|
||||
notesPlaceholder: "Add detailed notes, markdown supported...",
|
||||
notesPlaceholder: "Add details, markdown supported...",
|
||||
subtasks: "Subtasks",
|
||||
addSubtaskPlaceholder: "Add a subtask, press Enter...",
|
||||
deleteTaskConfirm: "Are you sure you want to delete this task?",
|
||||
addSubtaskPlaceholder: "Add a subtask...",
|
||||
deleteTaskConfirm: "Delete this task?",
|
||||
autoSaved: "Saved",
|
||||
saving: "Saving...",
|
||||
created: "Created",
|
||||
|
||||
@@ -155,28 +155,28 @@ export const ja: TranslationDict = {
|
||||
hideDone: "完了項目を非表示",
|
||||
showDone: "完了項目を表示",
|
||||
completedSection: "完了済み",
|
||||
addTaskPlaceholder: "新しいタスクを入力してEnter...",
|
||||
addTaskPlaceholder: "新しいタスクを書き留めましょう",
|
||||
add: "追加",
|
||||
noTasksYet: "このリストにはタスクがありません",
|
||||
selectListToStart: "リストを選択して始めましょう",
|
||||
selectListToStart: "リストを選択して開始してください",
|
||||
loading: "読み込み中...",
|
||||
today: "今日",
|
||||
tomorrow: "明日",
|
||||
|
||||
// Task Detail
|
||||
taskTitlePlaceholder: "タスク名を入力...",
|
||||
taskTitlePlaceholder: "タスク名",
|
||||
priority: "優先度",
|
||||
priorityNone: "なし",
|
||||
priorityLow: "低",
|
||||
priorityMedium: "中",
|
||||
priorityHigh: "高",
|
||||
dueDate: "期日",
|
||||
dueDate: "期限日",
|
||||
clear: "クリア",
|
||||
notes: "メモ",
|
||||
notesPlaceholder: "詳細なメモを入力 (Markdown対応)...",
|
||||
notesPlaceholder: "詳細を入力 (Markdown対応)...",
|
||||
subtasks: "サブタスク",
|
||||
addSubtaskPlaceholder: "サブタスクを入力してEnter...",
|
||||
deleteTaskConfirm: "このタスクを削除してもよろしいですか?",
|
||||
addSubtaskPlaceholder: "サブタスクを追加...",
|
||||
deleteTaskConfirm: "タスクを削除しますか?",
|
||||
autoSaved: "保存済み",
|
||||
saving: "保存中...",
|
||||
created: "作成日時",
|
||||
|
||||
@@ -155,7 +155,7 @@ export const ko: TranslationDict = {
|
||||
hideDone: "완료항목 숨기기",
|
||||
showDone: "완료항목 표시",
|
||||
completedSection: "완료됨",
|
||||
addTaskPlaceholder: "새 할 일 입력 후 Enter...",
|
||||
addTaskPlaceholder: "새로운 할 일을 적어볼까요?",
|
||||
add: "추가",
|
||||
noTasksYet: "이 목록에 등록된 할 일이 없습니다",
|
||||
selectListToStart: "시작할 목록을 선택하세요",
|
||||
@@ -164,7 +164,7 @@ export const ko: TranslationDict = {
|
||||
tomorrow: "내일",
|
||||
|
||||
// Task Detail
|
||||
taskTitlePlaceholder: "작업 제목 입력...",
|
||||
taskTitlePlaceholder: "할 일 제목",
|
||||
priority: "우선순위",
|
||||
priorityNone: "없음",
|
||||
priorityLow: "낮음",
|
||||
@@ -175,8 +175,8 @@ export const ko: TranslationDict = {
|
||||
notes: "메모",
|
||||
notesPlaceholder: "상세 내용을 입력하세요 (마크다운 지원)...",
|
||||
subtasks: "하위작업",
|
||||
addSubtaskPlaceholder: "하위작업 추가 후 Enter...",
|
||||
deleteTaskConfirm: "이 작업을 정말 삭제하시겠습니까?",
|
||||
addSubtaskPlaceholder: "하위 작업을 적어보세요...",
|
||||
deleteTaskConfirm: "태스크를 삭제하시겠습니까?",
|
||||
autoSaved: "저장됨",
|
||||
saving: "저장 중...",
|
||||
created: "생성일",
|
||||
|
||||
Reference in New Issue
Block a user