Skip to main content
Glama
translations.service.ts9.53 kB
/** * Translations Service * * Service layer for interacting with Lokalise Translations API. * Handles all API communication and data transformation. */ import type { CursorPaginatedResult, GetTranslationParams, ListTranslationParams, Translation, UpdateTranslationParams, } from "@lokalise/node-api"; import { createUnexpectedError } from "../../shared/utils/error.util.js"; import { Logger } from "../../shared/utils/logger.util.js"; import { getLokaliseApi } from "../../shared/utils/lokalise-api.util.js"; import type { BulkUpdateTranslationResult, BulkUpdateTranslationsSummary, BulkUpdateTranslationsToolArgsType, GetTranslationToolArgsType, ListTranslationsToolArgsType, UpdateTranslationToolArgsType, } from "./translations.types.js"; const logger = Logger.forContext("translations.service.ts"); // ============================================================================ // Service Implementation // ============================================================================ /** * Translations service for Lokalise API operations */ export const translationsService = { /** * List translations for a project * Note: Translations use cursor pagination, not standard pagination */ async list( args: ListTranslationsToolArgsType, ): Promise<CursorPaginatedResult<Translation>> { const methodLogger = logger.forMethod("list"); methodLogger.info("Listing translations", { projectId: args.projectId, cursor: args.cursor, limit: args.limit, filters: { langId: args.filterLangId, isReviewed: args.filterIsReviewed, unverified: args.filterUnverified, }, }); try { const api = getLokaliseApi(); // Build params with proper types const params: ListTranslationParams = { project_id: args.projectId, limit: args.limit, cursor: args.cursor, pagination: "cursor", // Force cursor pagination }; // Add optional filters if (args.filterLangId !== undefined) { params.filter_lang_id = args.filterLangId; } if (args.filterIsReviewed !== undefined) { params.filter_is_reviewed = args.filterIsReviewed; } if (args.filterUnverified !== undefined) { params.filter_unverified = args.filterUnverified; } if (args.filterUntranslated !== undefined) { params.filter_untranslated = args.filterUntranslated; } if (args.filterQaIssues !== undefined) { params.filter_qa_issues = args.filterQaIssues; } if (args.filterActiveTaskId !== undefined) { params.filter_active_task_id = args.filterActiveTaskId; } if (args.disableReferences !== undefined) { params.disable_references = args.disableReferences; } const result = await api.translations().list(params); methodLogger.info("Listed translations successfully", { projectId: args.projectId, count: result.items.length, hasNextCursor: result.hasNextCursor(), nextCursor: result.nextCursor, }); return result; } catch (error) { methodLogger.error("Failed to list translations", { error, args }); throw createUnexpectedError( `Failed to list translations for project ${args.projectId}`, error, ); } }, /** * Get a specific translation */ async get(args: GetTranslationToolArgsType): Promise<Translation> { const methodLogger = logger.forMethod("get"); methodLogger.info("Getting translation", { projectId: args.projectId, translationId: args.translationId, }); try { const api = getLokaliseApi(); // Build params const params: GetTranslationParams = { project_id: args.projectId, }; if (args.disableReferences !== undefined) { params.disable_references = args.disableReferences; } const result = await api.translations().get(args.translationId, params); methodLogger.info("Retrieved translation successfully", { projectId: args.projectId, translationId: args.translationId, keyId: result.key_id, languageIso: result.language_iso, }); return result; } catch (error) { methodLogger.error("Failed to get translation", { error, args }); throw createUnexpectedError( `Failed to get translation ${args.translationId} from project ${args.projectId}`, error, ); } }, /** * Update a translation */ async update(args: UpdateTranslationToolArgsType): Promise<Translation> { const methodLogger = logger.forMethod("update"); methodLogger.info("Updating translation", { projectId: args.projectId, translationId: args.translationId, data: args.translationData, }); try { const api = getLokaliseApi(); // Build update data with SDK types const updateData: UpdateTranslationParams = { translation: args.translationData.translation, }; // Map optional fields if (args.translationData.isReviewed !== undefined) { updateData.is_reviewed = args.translationData.isReviewed; } if (args.translationData.isUnverified !== undefined) { updateData.is_unverified = args.translationData.isUnverified; } if (args.translationData.customTranslationStatusIds !== undefined) { updateData.custom_translation_status_ids = args.translationData.customTranslationStatusIds; } const result = await api .translations() .update(args.translationId, updateData, { project_id: args.projectId }); methodLogger.info("Updated translation successfully", { projectId: args.projectId, translationId: args.translationId, isReviewed: result.is_reviewed, isUnverified: result.is_unverified, }); return result; } catch (error) { methodLogger.error("Failed to update translation", { error, args }); throw createUnexpectedError( `Failed to update translation ${args.translationId} in project ${args.projectId}`, error, ); } }, /** * Bulk update multiple translations with rate limiting and retry logic */ async bulkUpdate( args: BulkUpdateTranslationsToolArgsType, ): Promise<BulkUpdateTranslationsSummary> { const methodLogger = logger.forMethod("bulkUpdate"); methodLogger.info("Starting bulk translation update", { projectId: args.projectId, updateCount: args.updates.length, }); const startTime = Date.now(); const results: BulkUpdateTranslationResult[] = []; const RATE_LIMIT_DELAY = 200; // 200ms delay = max 5 requests/second const MAX_RETRY_ATTEMPTS = 3; const RETRY_DELAY = 1000; // 1 second delay between retries // Helper function to update a single translation with retry logic const updateWithRetry = async ( update: (typeof args.updates)[0], attemptNumber = 1, ): Promise<BulkUpdateTranslationResult> => { try { const updateArgs: UpdateTranslationToolArgsType = { projectId: args.projectId, translationId: update.translationId, translationData: update.translationData, }; const translation = await this.update(updateArgs); return { translationId: update.translationId, success: true, translation, attempts: attemptNumber, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (attemptNumber < MAX_RETRY_ATTEMPTS) { methodLogger.warn( `Translation update failed, retrying (attempt ${attemptNumber}/${MAX_RETRY_ATTEMPTS})`, { translationId: update.translationId, error: errorMessage, attemptNumber, }, ); // Wait before retry await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); // Retry return updateWithRetry(update, attemptNumber + 1); } // Max retries reached, record as failure methodLogger.error("Translation update failed after max retries", { translationId: update.translationId, error: errorMessage, attempts: attemptNumber, }); return { translationId: update.translationId, success: false, error: errorMessage, attempts: attemptNumber, }; } }; // Process updates sequentially with rate limiting for (let i = 0; i < args.updates.length; i++) { const update = args.updates[i]; methodLogger.info( `Processing translation ${i + 1}/${args.updates.length}`, { translationId: update.translationId, }, ); // Perform the update const result = await updateWithRetry(update); results.push(result); // Rate limiting: delay before next request (except for the last one) if (i < args.updates.length - 1) { await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_DELAY)); } } // Calculate summary const duration = Date.now() - startTime; const successCount = results.filter((r) => r.success).length; const failureCount = results.filter((r) => !r.success).length; const summary: BulkUpdateTranslationsSummary = { totalRequested: args.updates.length, successCount, failureCount, results, duration, }; methodLogger.info("Bulk translation update completed", { projectId: args.projectId, totalRequested: summary.totalRequested, successCount: summary.successCount, failureCount: summary.failureCount, durationMs: summary.duration, }); return summary; }, }; /** * Implementation Notes: * * 1. Translations use CURSOR PAGINATION, not standard page/limit pagination * 2. Filter by language ID requires numeric ID, not ISO code * 3. Review and verification statuses are important for translation workflow * 4. Translation content can be string or object (for plural forms) * 5. Custom translation statuses allow for custom workflow states */

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AbdallahAHO/lokalise-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server