lokalise-mcp
Version:
The Lokalise MCP Server brings Lokalise's localization power to Claude and AI assistants—manage projects, keys, and translations by chat.
614 lines (613 loc) • 24.2 kB
JavaScript
import { formatBulletList, formatDate, formatEmptyState, formatFooter, formatHeading, formatPaginationInfo, formatTable, } from "../../shared/utils/formatter.util.js";
/**
* @namespace TasksFormatter
* @description Utility functions for formatting Lokalise Tasks API responses into readable formats.
* Provides consistent Markdown formatting for various task operations.
*/
/**
* @function formatTasksList
* @description Formats a list of tasks into a comprehensive, LLM-friendly Markdown report
* @memberof TasksFormatter
* @param {PaginatedResult<Task>} response - The raw response from the Lokalise Tasks API list operation
* @param {string} projectId - The project ID for context
* @returns {string} Formatted Markdown string containing the tasks list
*/
export function formatTasksList(response, projectId) {
const tasks = response.items || [];
const hasNextCursor = response.hasNextPage();
const lines = [];
lines.push(formatHeading("Tasks Analysis", 1));
lines.push("");
lines.push(formatHeading(`Project: ${projectId}`, 2));
lines.push("");
if (tasks.length === 0) {
const suggestions = [
"The project has no tasks created yet",
"All tasks have been deleted",
"Filter criteria excluded all tasks",
"You may not have permission to view tasks",
];
lines.push(formatEmptyState("tasks", "this project", suggestions));
lines.push(formatFooter("Analysis completed"));
return lines.join("\n");
}
// Executive Summary
lines.push(formatHeading("Executive Summary", 2));
lines.push("");
lines.push(`**${tasks.length} tasks** found in this project.`);
lines.push("");
// Pagination information
lines.push(formatPaginationInfo(hasNextCursor, response.nextPage(), tasks.length));
// Tasks table
lines.push(formatHeading("Task Inventory", 2));
lines.push("");
const tableData = tasks.map((task) => ({
id: task.task_id || "N/A",
title: task.title || "Untitled",
status: task.status || "Unknown",
type: task.task_type || "Unknown",
sourceLanguage: task.source_language_iso || "Unknown",
progress: typeof task.progress === "number" ? `${task.progress}%` : "N/A",
keys: typeof task.keys_count === "number" ? task.keys_count : "N/A",
words: typeof task.words_count === "number" ? task.words_count : "N/A",
dueDate: formatDueDate(task.due_date),
created: formatCreatedDate(task.created_at),
}));
const table = formatTable(tableData, [
{ key: "id", header: "ID" },
{ key: "title", header: "Title", maxWidth: 30 },
{ key: "status", header: "Status" },
{ key: "type", header: "Type" },
{ key: "progress", header: "Progress" },
{ key: "keys", header: "Keys" },
{ key: "words", header: "Words" },
{ key: "dueDate", header: "Due Date" },
{ key: "created", header: "Created" },
]);
lines.push(table);
lines.push("");
// Detailed snapshots per task covering all fields
lines.push(formatHeading("Detailed Task Snapshots", 2));
lines.push("");
for (const task of tasks) {
lines.push(formatHeading(`Task #${task.task_id} – ${task.title || "Untitled"}`, 3));
lines.push(formatTaskSnapshot(task));
lines.push("");
}
// Summary
lines.push(formatHeading("Summary", 2));
lines.push("");
lines.push(`**Project ${projectId} has ${tasks.length} tasks** in this view.`);
lines.push("");
// Footer
lines.push(formatFooter("Analysis completed", `Showing ${tasks.length} tasks from project \`${projectId}\``));
if (hasNextCursor) {
lines.push("*Additional tasks available - use cursor pagination to fetch more*");
}
return lines.join("\n");
}
/**
* @function formatTaskDetails
* @description Formats detailed information about a single task with comprehensive LLM-friendly output
* @memberof TasksFormatter
* @param {Task} task - The task object from Lokalise API
* @param {string} projectId - The project ID for context
* @returns {string} Formatted Markdown string containing the task details
*/
export function formatTaskDetails(task, projectId) {
const lines = [];
// Main heading
lines.push(formatHeading("Task Details", 1));
lines.push("");
// Core identification
lines.push(formatHeading("Core Information", 2));
const coreInfo = {
"Task ID": task.task_id,
"Project ID": `\`${projectId}\``,
Title: task.title || "*No title provided*",
Description: task.description || "*No description provided*",
Type: task.task_type || "Unknown",
Status: task.status || "Unknown",
"Can Be Parent": boolToYesNo(task.can_be_parent),
"Parent Task ID": task.parent_task_id ?? "None",
"Closing Tags": formatArray(task.closing_tags),
"Lock Translations": boolToYesNo(task.do_lock_translations),
"Custom Status IDs": formatArray(task.custom_translation_status_ids),
};
lines.push(formatBulletList(coreInfo));
lines.push("");
// Schedule information
lines.push(formatHeading("Schedule Information", 2));
const scheduleInfo = formatScheduleInfo(task);
lines.push(scheduleInfo);
if (task.created_at) {
// Timeline information
const timeline = {
Created: formatCreatedDateWithTime(task.created_at),
};
lines.push(formatBulletList(timeline));
lines.push("");
}
// Language assignments (detailed)
lines.push(formatHeading("Language Assignments", 2));
if (task.languages && task.languages.length > 0) {
lines.push(`**Target Languages:** ${task.languages.length}`);
lines.push("");
for (const language of task.languages) {
lines.push(formatLanguageDetails(language));
}
}
else {
lines.push("*No languages assigned to this task*");
lines.push("");
}
// Task configuration & metrics
lines.push(formatHeading("Configuration & Metrics", 2));
const configInfo = {
"Source Language": task.source_language_iso || "Not specified",
"Auto-close Languages": boolToYesNo(task.auto_close_languages),
"Auto-close Task": boolToYesNo(task.auto_close_task),
"Auto-close Items": boolToYesNo(task.auto_close_items),
"Total Keys": task.keys_count ?? 0,
"Total Words": task.words_count ?? 0,
"Overall Progress": typeof task.progress === "number" ? `${task.progress}%` : "N/A",
};
lines.push(formatBulletList(configInfo));
lines.push("");
// Audit & completion
lines.push(formatHeading("Audit", 2));
const auditInfo = {
"Created By": task.created_by ?? "Unknown",
"Created By Email": task.created_by_email ?? "Unknown",
"Created At": task.created_at
? formatCreatedDateWithTime(task.created_at)
: "Unknown",
"Created At (ts)": task.created_at_timestamp ?? "N/A",
"Completed By": task.completed_by ?? "N/A",
"Completed By Email": task.completed_by_email ?? "N/A",
"Completed At": task.completed_at
? formatCreatedDateWithTime(task.completed_at)
: "N/A",
"Completed At (ts)": task.completed_at_timestamp ?? "N/A",
};
lines.push(formatBulletList(auditInfo));
lines.push("");
// Summary for LLM reasoning
lines.push(formatHeading("Task Summary", 2));
lines.push("**Task Characteristics:**");
lines.push(`- ${task.languages?.length || 0} language(s) targeted`);
if (task.due_date) {
try {
const dueDate = new Date(task.due_date);
const isOverdue = dueDate < new Date();
lines.push(`- ${isOverdue ? "⚠️ OVERDUE" : "✅ On schedule"} deadline: ${dueDate.toLocaleDateString()}`);
}
catch {
lines.push("- ⚠️ Invalid due date format");
}
}
else {
lines.push("- ⚠️ No deadline set");
}
lines.push(`- Current status: ${task.status}`);
lines.push(`- Task type: ${task.task_type || "Unknown"}`);
lines.push("");
// Footer
lines.push(formatFooter("Task details retrieved", `Task \`${task.task_id}\` from project \`${projectId}\``));
return lines.join("\n");
}
/**
* @function formatCreateTaskResult
* @description Formats the result of creating a task
* @memberof TasksFormatter
* @param {Task} task - The created task object from Lokalise API
* @param {string} projectId - The project ID for context
* @returns {string} Formatted Markdown string containing the creation results
*/
export function formatCreateTaskResult(task, projectId) {
const lines = [];
// Main heading
lines.push(formatHeading("Task Created Successfully", 1));
lines.push("");
// Created task information
lines.push(formatHeading("Created Task Information", 2));
const taskInfo = {
"Task ID": task.task_id,
Project: `\`${projectId}\``,
Title: task.title,
Type: task.task_type || "Unknown",
Status: task.status,
};
if (task.created_at) {
taskInfo.Created = formatCreatedDateWithTime(task.created_at);
}
if (task.due_date) {
taskInfo["Due Date"] = formatDueDateWithTime(task.due_date);
}
lines.push(formatBulletList(taskInfo));
lines.push("");
// Language coverage
if (task.languages && task.languages.length > 0) {
lines.push(formatHeading("Language Coverage", 2));
lines.push(`**${task.languages.length} language(s) configured:**`);
lines.push("");
for (const language of task.languages) {
const userCount = language.users?.length || 0;
const groupCount = language.groups?.length || 0;
lines.push(`- **${language.language_iso.toUpperCase()}:** ${userCount} user(s), ${groupCount} group(s)`);
}
lines.push("");
}
// Next steps
const nextSteps = [
"Notify assigned team members about the new task",
"Review task details and assignments",
"Monitor progress as work begins",
];
if (task.due_date) {
try {
const dueDate = new Date(task.due_date);
const daysUntilDue = Math.ceil((dueDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
if (daysUntilDue <= 7) {
nextSteps.push("⚠️ **Due date is approaching** - ensure team is aware of the deadline");
}
}
catch {
// Ignore date parsing errors
}
}
else {
nextSteps.push("Consider setting a due date for better project management");
}
lines.push(formatHeading("Next Steps", 2));
lines.push("✅ **Task created successfully**");
lines.push("");
lines.push("**Recommended actions:**");
for (const step of nextSteps) {
lines.push(`- ${step}`);
}
lines.push("");
// Footer
lines.push(formatFooter("Task created"));
return lines.join("\n");
}
/**
* @function formatUpdateTaskResult
* @description Formats the result of updating a task
* @memberof TasksFormatter
* @param {Task} task - The updated task object from Lokalise API
* @param {string} projectId - The project ID for context
* @returns {string} Formatted Markdown string containing the update results
*/
export function formatUpdateTaskResult(task, projectId) {
const lines = [];
// Main heading
lines.push(formatHeading("Task Updated Successfully", 1));
lines.push("");
// Updated task information
lines.push(formatHeading("Updated Task Information", 2));
const taskInfo = {
"Task ID": task.task_id,
Project: `\`${projectId}\``,
Title: task.title,
Status: task.status,
};
if (task.created_at) {
taskInfo["Last Modified"] = formatCreatedDateWithTime(task.created_at);
}
if (task.due_date) {
taskInfo["Due Date"] = formatDueDateDetailed(task.due_date);
}
lines.push(formatBulletList(taskInfo));
lines.push("");
if (task.languages && task.languages.length > 0) {
// Language assignments
lines.push(formatHeading("Language Assignments", 2));
lines.push(`**${task.languages.length} language(s) configured:**`);
lines.push("");
for (const language of task.languages) {
const userCount = language.users?.length || 0;
const groupCount = language.groups?.length || 0;
lines.push(`- **${language.language_iso.toUpperCase()}:** ${userCount} user(s), ${groupCount} group(s)`);
}
lines.push("");
}
// Update confirmation
lines.push(formatHeading("Update Confirmation", 2));
lines.push("✅ **Task updated successfully**");
lines.push("");
lines.push("**Changes have been applied to:**");
lines.push("- Task metadata and configuration");
lines.push("- Assignment and language settings");
lines.push("- Due date and priority settings");
lines.push("");
if (task.status === "closed") {
lines.push("🎉 **Task has been closed** - work is complete!");
lines.push("");
}
else if (task.status === "completed") {
lines.push("✅ **Task marked as completed** - ready for final review");
lines.push("");
}
// Footer
lines.push(formatFooter("Task updated"));
return lines.join("\n");
}
/**
* @function formatDeleteTaskResult
* @description Formats the result of deleting a task
* @memberof TasksFormatter
* @param {TaskDeleted} result - The deletion result from Lokalise API
* @param {string} projectId - The project ID for context
* @param {number} taskId - The ID of the deleted task
* @returns {string} Formatted Markdown string containing the deletion confirmation
*/
export function formatDeleteTaskResult(result, projectId, taskId) {
const lines = [];
// Main heading
lines.push(formatHeading("Task Deleted Successfully", 1));
lines.push("");
// Deletion confirmation
lines.push(formatHeading("Deletion Confirmation", 2));
const deletionInfo = {
"Task ID": taskId,
Project: `\`${projectId}\``,
Status: "✅ **DELETED**",
Deleted: result.task_deleted ? "Yes" : "No",
Timestamp: formatDate(new Date()),
};
lines.push(formatBulletList(deletionInfo));
lines.push("");
// Important notes
lines.push(formatHeading("Important Notes", 2));
lines.push("⚠️ **This action is permanent** - the task and all its associated data have been permanently removed.");
lines.push("");
lines.push("**What was deleted:**");
lines.push("- Task metadata (title, description, settings)");
lines.push("- All task assignments");
lines.push("- Language-specific assignments");
lines.push("- Task history and progress tracking");
lines.push("");
lines.push("**What was NOT affected:**");
lines.push("- Translation keys and their content remain unchanged");
lines.push("- Project settings and other tasks are unaffected");
lines.push("- User accounts and permissions remain as they were");
lines.push("");
// Cleanup complete
lines.push(formatHeading("Cleanup Complete", 2));
lines.push("🗑️ **Task removal successful** - the task has been permanently deleted from your project.");
lines.push("");
// Footer
lines.push(formatFooter("Task deleted"));
return lines.join("\n");
}
// Helper functions for date formatting
function formatDueDate(dueDate) {
if (!dueDate)
return "No deadline";
try {
const due = new Date(dueDate);
const isOverdue = due < new Date();
const formatted = due.toLocaleDateString();
return isOverdue ? `🔴 ${formatted}` : `📅 ${formatted}`;
}
catch {
return "Invalid date";
}
}
function formatCreatedDate(createdAt) {
if (!createdAt)
return "Unknown";
try {
return new Date(createdAt).toLocaleDateString();
}
catch {
return "Invalid date";
}
}
function formatCreatedDateWithTime(createdAt) {
if (!createdAt)
return "Invalid date format";
try {
return `${createdAt} (${new Date(createdAt).toLocaleDateString()})`;
}
catch {
return "Invalid date format";
}
}
function formatDueDateWithTime(dueDate) {
if (!dueDate)
return "No deadline set";
try {
const due = new Date(dueDate);
return `${due.toLocaleDateString()} at ${due.toLocaleTimeString()}`;
}
catch {
return "Invalid date format";
}
}
function formatDueDateDetailed(dueDate) {
if (!dueDate)
return "No deadline set";
try {
const due = new Date(dueDate);
const now = new Date();
const isOverdue = due < now;
const daysDiff = Math.ceil((due.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
let status;
if (isOverdue) {
status = ` (🔴 **OVERDUE** by ${Math.abs(daysDiff)} day${Math.abs(daysDiff) !== 1 ? "s" : ""})`;
}
else if (daysDiff <= 1) {
status = " (🟡 **DUE SOON**)";
}
else {
status = " (✅ On schedule)";
}
return `${due.toLocaleDateString()} at ${due.toLocaleTimeString()}${status}`;
}
catch {
return "Invalid date format";
}
}
function formatScheduleInfo(task) {
const lines = [];
if (task.due_date) {
try {
const dueDate = new Date(task.due_date);
const now = new Date();
const isOverdue = dueDate < now;
const daysDiff = Math.ceil((dueDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
const scheduleInfo = {
"Due Date": `${dueDate.toLocaleDateString()} at ${dueDate.toLocaleTimeString()}`,
};
if (isOverdue) {
scheduleInfo.Status = `🔴 **OVERDUE** by ${Math.abs(daysDiff)} day${Math.abs(daysDiff) !== 1 ? "s" : ""}`;
}
else if (daysDiff <= 1) {
scheduleInfo.Status = `🟡 **DUE SOON** (${daysDiff === 0 ? "today" : "tomorrow"})`;
}
else if (daysDiff <= 7) {
scheduleInfo.Status = `📅 **DUE THIS WEEK** (in ${daysDiff} days)`;
}
else {
scheduleInfo.Status = `✅ **ON SCHEDULE** (in ${daysDiff} days)`;
}
lines.push(formatBulletList(scheduleInfo));
}
catch {
const errorInfo = {
"Due Date": "Invalid date format",
};
lines.push(formatBulletList(errorInfo));
}
}
else {
const noDeadlineInfo = {
"Due Date": "No deadline set",
Status: "⚠️ **NO DEADLINE** - consider setting a due date",
};
lines.push(formatBulletList(noDeadlineInfo));
}
lines.push("");
return lines.join("\n");
}
// --- Additional rich formatters ---
function boolToYesNo(value) {
return value ? "Yes" : "No";
}
function formatArray(arr) {
if (!arr || arr.length === 0)
return "None";
return arr.join(", ");
}
function formatTaskSnapshot(task) {
const lines = [];
const meta = {
Status: task.status || "Unknown",
Type: task.task_type || "Unknown",
Progress: typeof task.progress === "number" ? `${task.progress}%` : "N/A",
"Due Date": task.due_date
? formatDueDateWithTime(task.due_date)
: "No deadline",
"Due Date (ts)": task.due_date_timestamp ?? "N/A",
"Created At": task.created_at
? formatCreatedDateWithTime(task.created_at)
: "Unknown",
"Created At (ts)": task.created_at_timestamp ?? "N/A",
"Created By": task.created_by ?? "Unknown",
"Created By Email": task.created_by_email ?? "Unknown",
"Completed At": task.completed_at
? formatCreatedDateWithTime(task.completed_at)
: "N/A",
"Completed At (ts)": task.completed_at_timestamp ?? "N/A",
"Completed By": task.completed_by ?? "N/A",
"Completed By Email": task.completed_by_email ?? "N/A",
"Can Be Parent": boolToYesNo(task.can_be_parent),
"Parent Task ID": task.parent_task_id ?? "None",
"Closing Tags": formatArray(task.closing_tags),
"Lock Translations": boolToYesNo(task.do_lock_translations),
"Custom Status IDs": formatArray(task.custom_translation_status_ids),
"Source Language": task.source_language_iso || "Not specified",
"Auto-close Languages": boolToYesNo(task.auto_close_languages),
"Auto-close Task": boolToYesNo(task.auto_close_task),
"Auto-close Items": boolToYesNo(task.auto_close_items),
"Total Keys": task.keys_count ?? 0,
"Total Words": task.words_count ?? 0,
"Target Languages": task.languages?.length ?? 0,
};
lines.push(formatBulletList(meta));
lines.push("");
if (task.languages && task.languages.length > 0) {
lines.push("**Languages:**");
for (const language of task.languages) {
lines.push(`- ${language.language_iso?.toUpperCase() || "Unknown"} • status: ${language.status || "Unknown"} • progress: ${typeof language.progress === "number" ? `${language.progress}%` : "N/A"} • keys: ${language.keys_count ?? 0} • words: ${language.words_count ?? 0}`);
}
lines.push("");
}
return lines.join("\n");
}
function formatLanguageDetails(language) {
const lines = [];
lines.push(formatHeading(language.language_iso.toUpperCase(), 3));
lines.push("");
const langInfo = {
Status: language.status || "Unknown",
Progress: typeof language.progress === "number" ? `${language.progress}%` : "N/A",
"Keys Count": language.keys_count ?? 0,
"Words Count": language.words_count ?? 0,
"Completed At": language.completed_at || "N/A",
"Completed At (ts)": language.completed_at_timestamp ?? "N/A",
"Completed By": language.completed_by ?? "N/A",
"Completed By Email": language.completed_by_email ?? "N/A",
};
lines.push(formatBulletList(langInfo));
lines.push("");
if (Array.isArray(language.keys) && language.keys.length > 0) {
lines.push(`Keys scope: ${language.keys.length} key(s)`);
}
if (language.users && language.users.length > 0) {
lines.push(`Assigned Users (${language.users.length}):`);
for (const user of language.users) {
lines.push(`- ${user.fullname || user.email || `ID: ${user.user_id}`}`);
}
lines.push("");
}
if (language.groups && language.groups.length > 0) {
lines.push(`Assigned Groups (${language.groups.length}):`);
for (const group of language.groups) {
lines.push(`- ${group.name || `ID: ${group.id}`}`);
}
lines.push("");
}
// Leverage
if (language.initial_tm_leverage) {
lines.push("Initial TM Leverage:");
lines.push(formatLeverageBuckets(language.initial_tm_leverage));
lines.push("");
}
if (language.tm_leverage) {
lines.push(`TM Leverage Status: ${language.tm_leverage.status || "Unknown"}`);
if (language.tm_leverage.value) {
lines.push(formatLeverageBuckets(language.tm_leverage.value));
}
lines.push("");
}
if ((!language.users || language.users.length === 0) &&
(!language.groups || language.groups.length === 0)) {
lines.push("*No specific users or groups assigned to this language*");
lines.push("");
}
return lines.join("\n");
}
function formatLeverageBuckets(value) {
const orderedKeys = ["0%+", "50%+", "60%+", "75%+", "85%+", "95%+", "100%"];
const present = orderedKeys.filter((k) => k in value);
if (present.length === 0)
return "- No leverage data";
const lines = [];
for (const k of present) {
lines.push(`- ${k}: ${value[k]}%`);
}
return lines.join("\n");
}