diff --git a/docs/03-modules-and-sections.md b/docs/03-modules-and-sections.md index f9ad01a..2d01cce 100644 --- a/docs/03-modules-and-sections.md +++ b/docs/03-modules-and-sections.md @@ -203,6 +203,16 @@ Acceso en Twig: Las variables son **propiedades del objeto iterado**, no variables sueltas. +## Traducir las variables de un módulo + +Los valores textuales de las variables de un módulo (títulos, descripciones, wysiwyg, etc.) NO se guardan en la fila de la página, sino en la tabla `builder_custom`. Por eso una traducción de módulo apunta siempre a `builder_custom`, no a `apartados` ni a la tabla de la página. + +Para traducir las vars de una instancia de módulo: +1. `get_module_config_vars({ tableName, recordNum, sectionId })` devuelve `varsMeta`: por cada variable, su ubicación física `{ fieldName, recordNum }` en `builder_custom` (y por cada item en las vars multi). +2. `set_record_translations({ tableName: "builder_custom", recordNum: , prefix, fields: { : "texto traducido" } })`. + +El nombre humano de la variable (p.ej. `titulo`) NO es el nombre de columna real (p.ej. `title2`): usa siempre el `fieldName` que da `varsMeta`. Ver el workflow completo en `09-mcp-tools-reference.md`. + ## Layout global vs módulos `header`, `footer`, `style` global y `javascript` global NO son módulos normales. Viven en `cms/lib/plugins/builder_saas/layout.json` y se editan con tools dedicadas (`get_layout_field` / `set_layout_field`). Ver `08-layout-and-libraries.md`. diff --git a/docs/04-pages-and-records.md b/docs/04-pages-and-records.md index 7d07d72..4137556 100644 --- a/docs/04-pages-and-records.md +++ b/docs/04-pages-and-records.md @@ -152,6 +152,20 @@ create_or_update_record: metatag_descripcion: "Descubre nuestros servicios…" ``` +## Traducciones (multiidioma) + +En sitios multiidioma las traducciones NO viven en la fila del registro: se guardan en la tabla central `cms_traducciones`, indexadas por `(prefix = código de idioma, tableName sin cms_, fieldName, recordNum)`. El valor (`fieldValue`) se almacena en Base64, pero es transparente: las tools y el motor codifican/decodifican por ti, tú siempre trabajas con texto plano. + +- El idioma base usa el prefix `www` y sus URLs no llevan prefijo. Los idiomas secundarios (p.ej. `en`) sirven bajo `/en/...`. Consulta los activos con `list_web_languages`. +- Solo se traducen los tipos de campo traducibles: `textfield`, `textbox`, `wysiwyg`, `codigo` y `multitext` (este último se traduce como el JSON serializado completo en una sola fila). +- Un valor vacío (`''`) en `set_record_translations` **borra** la traducción y el runtime cae al idioma base. +- Para leer un registro ya traducido, pasa `lang: ""` a `get_record` o `list_table_records`. +- El campo `identificador` de `textos_generales` (los literales del filtro Twig `| translate`) se traduce por su campo `texto`. + +### Regla dura: nunca traduzcas `enlace` + +El campo `enlace` NUNCA se traduce a mano — lo mantiene el motor CocoEnlace. La action PHP rechaza cualquier intento de traducir `enlace`. No lo incluyas en `set_record_translations`. + ## Patrón canónico — Detalle de registro Para cualquier tabla con campo `enlace` (productos, noticias, vacantes, servicios), **el detalle se resuelve por convención** vía sección general `custom-{tableName}`. Ver `03-modules-and-sections.md` para detalles. diff --git a/docs/06-hooks-and-cmsapi.md b/docs/06-hooks-and-cmsapi.md index 5f15b8a..a834868 100644 --- a/docs/06-hooks-and-cmsapi.md +++ b/docs/06-hooks-and-cmsapi.md @@ -149,7 +149,7 @@ $datos = CmsApi::get("productos", "", "", "", [ | `uploads` | bool | `true` | Incluir datos de upload fields | | `relations` | bool/array | `true` | Resolver foreign keys. Array para limitar: `['categoria']` | | `relationsDepth` | int | 2 | Profundidad de relaciones anidadas | -| `translates` | string | idioma actual | Código de idioma para `| translate` | +| `translates` | string/bool | idioma actual | Código de idioma (prefix, p.ej. `'en'`) para devolver los campos ya traducidos desde `cms_traducciones`. `true` usa el idioma activo de la request | | `groupBy` | string | null | Cláusula GROUP BY | | `aggregates` | array | `[]` | Funciones de agregación | | `onlyFields` | array | null | Seleccionar solo ciertos campos | @@ -157,6 +157,8 @@ $datos = CmsApi::get("productos", "", "", "", [ | `redis` | bool | null | Forzar cache Redis | | `redis_expire` | int | 60 | TTL del cache (segundos) | +**Idioma en lectura**: pasar `options['translates'] => ''` hace que `CmsApi::get`/`cmsApi` devuelva los valores traducidos de los campos traducibles. Las tools MCP `list_table_records` y `get_record` exponen esto con su parámetro `lang` (que se traduce a `options.translates`). A nivel HTTP, `cms_api` v3 también fuerza el idioma con el header `X-ACAI-ACCEPT-LANGUAGE: `, que tiene el mismo efecto que `translates` para toda la request. Ver `09-mcp-tools-reference.md` (sección Idiomas y traducciones) para el flujo de escritura con `set_record_translations`. + ### Insert — `CmsApi::insert()` ```php diff --git a/docs/09-mcp-tools-reference.md b/docs/09-mcp-tools-reference.md index 4897e28..2e81aa1 100644 --- a/docs/09-mcp-tools-reference.md +++ b/docs/09-mcp-tools-reference.md @@ -153,6 +153,20 @@ Tools del MCP `playwright`. El browser headless es del agente — el usuario NO |------|--------| | `refresh_acai_token` | Renueva el JWT cuando expira (errores 403) | +### Idiomas y traducciones + +Las webs multiidioma guardan las traducciones en la tabla central `cms_traducciones`. Ver `04-pages-and-records.md` y `11b-rules-cheat-sheet.md`. + +| Tool | Acción | Notas | +|------|--------|-------| +| `list_web_languages` | Lista los idiomas activos del sitio (`settings.dat.php [idiomas]`) | Devuelve `prefix`/`urlPrefix`/`isDefault`. `prefix="www"` = idioma base (URLs sin prefijo); otro prefix (p.ej. `en`) = idioma bajo `/en/...` | +| `get_record_translations` | Lee traducciones de uno o varios registros | Por `tableName` (sin `cms_`) + `recordNums`. Opcional `fields` y `prefix`. Devuelve `{recordNum:{prefix:{fieldName:valor}}}` ya decodificado | +| `set_record_translations` | Escribe traducciones de un registro + idioma | `tableName`+`recordNum`+`prefix`+`fields`. `''` borra la traducción. **`enlace` prohibido** (lo mantiene CocoEnlace) | + +Además, las tools de lectura `list_table_records` y `get_record` aceptan el parámetro opcional `lang` (prefix, p.ej. `'en'`): devuelven los valores ya traducidos de los campos traducibles (el motor CocoDB aplica la traducción en lectura). + +Campos traducibles por tipo: `textfield`, `textbox`, `wysiwyg`, `codigo`, `multitext` (el multitext se traduce como el JSON serializado completo en una sola fila). El valor en la DB va en Base64, pero es transparente: pasas y recibes texto plano. + ### Documentación | Tool | Acción | @@ -279,6 +293,32 @@ Notas: - En modo producción todas estas tools sincronizan automáticamente con el servidor real (no solo modifican local). - Si solo tienes el `recordId` y necesitas saber qué `fieldName` tiene uploads, llama antes a `get_table_schema({ minimal: true })` y filtra los campos `type: "upload"`. +### 12. Traducir un registro a otro idioma + +Ejemplo: traducir la vacante num=12 al inglés. + +1. `list_web_languages` — obtén el `prefix` del idioma destino (p.ej. `en`). El prefix `www` es el idioma base y NO se traduce por aquí (se edita con los campos normales del registro). +2. (Opcional) `get_record({ tableName: "vacantes", recordNum: 12 })` para leer los textos originales, o `get_record_translations({ tableName: "vacantes", recordNums: [12], prefix: "en" })` para ver qué falta. +3. `set_record_translations({ tableName: "vacantes", recordNum: 12, prefix: "en", fields: { titulo: "...", descripcion: "..." } })`. Solo campos traducibles (`textfield`, `textbox`, `wysiwyg`, `codigo`, `multitext`). **NUNCA** incluyas `enlace`. Un `''` borra esa traducción. +4. Verifica con `get_record({ tableName: "vacantes", recordNum: 12, lang: "en" })` — devuelve los valores ya traducidos. + +### 13. Traducir las variables de un módulo + +Los textos de un módulo Builder viven en la tabla `builder_custom`, no en la tabla de la página. + +1. `list_web_languages` — obtén el `prefix` destino. +2. `get_module_config_vars({ tableName, recordNum, sectionId })` — devuelve `varsMeta`: por cada variable su `{ fieldName, recordNum }` físico en `builder_custom` (y por item en vars multi). +3. Por cada variable a traducir, `set_record_translations({ tableName: "builder_custom", recordNum: , prefix, fields: { : "texto traducido" } })`. +4. Para vars multi, repite con el `recordNum`/`fieldName` de cada item que devuelve `varsMeta`. + +### 14. Traducir un texto general (literal de plantilla) + +Los literales del filtro Twig `| translate` son registros normales de la tabla `textos_generales`; se traduce su campo `texto`. + +1. `list_web_languages` — obtén el `prefix` destino. +2. `list_table_records({ tableName: "textos_generales", where: "identificador = '...'", fields: ["num", "identificador", "texto"] })` para localizar el `num`. +3. `set_record_translations({ tableName: "textos_generales", recordNum: , prefix, fields: { texto: "traducción" } })`. + ## Reglas globales para todas las tools 1. **`tableName` siempre SIN prefijo `cms_`** (excepto en `queryDB` Twig y en el `middleWare` de `set_hook_middleware`). diff --git a/docs/11b-rules-cheat-sheet.md b/docs/11b-rules-cheat-sheet.md index 9250255..300dba9 100644 --- a/docs/11b-rules-cheat-sheet.md +++ b/docs/11b-rules-cheat-sheet.md @@ -87,6 +87,20 @@ Resumen ejecutable de reglas críticas, tipos de campo, filtros y formatos de da | `multitext` | String JSON | `"[{\"item\":\"valor\"}]"` | | `upload` | NO enviar — usar `upload_record_image` después | +## Traducciones (multiidioma) + +| Regla | Detalle | +|-------|---------| +| Prefix `www` = idioma base | URLs sin prefijo. Otro prefix (p.ej. `en`) sirve bajo `/en/...` — lista con `list_web_languages` | +| Nunca traducir `enlace` | Lo mantiene CocoEnlace; la action PHP lo rechaza | +| Valor `''` borra la traducción | `set_record_translations` con `''` elimina la fila y cae al idioma base | +| Base64 transparente | El valor se guarda Base64 en `cms_traducciones`; tú siempre pasas/recibes texto plano | +| `multitext` = JSON completo | Se traduce el JSON serializado entero en una sola fila, no item por item | +| Campos traducibles | Solo `textfield`, `textbox`, `wysiwyg`, `codigo`, `multitext` | +| Leer traducido | `get_record`/`list_table_records` con `lang: ""` | +| Vars de módulo | Se traducen sobre `builder_custom` usando `varsMeta` de `get_module_config_vars` | +| Textos generales | Traducir el campo `texto` del registro en `textos_generales` (localiza por `identificador`) | + ## Variables globales en Twig | Variable | Descripción | diff --git a/mcp-server/tools/helpers/ACAI_ENDPOINTS.md b/mcp-server/tools/helpers/ACAI_ENDPOINTS.md index 986e9a0..8ec7360 100644 --- a/mcp-server/tools/helpers/ACAI_ENDPOINTS.md +++ b/mcp-server/tools/helpers/ACAI_ENDPOINTS.md @@ -323,6 +323,75 @@ action=uploadModify **Usado por**: `delete_record_upload` **Headers**: `X-Acai-Token`, `X-Requested-With: XMLHttpRequest` +## Categoría: Idiomas y traducciones (multiidioma) + +Las traducciones viven en la tabla central `cms_traducciones`, indexada por `(prefix, tableName sin cms_, fieldName, recordNum)`. `fieldValue` se guarda en Base64, pero la codificación/decodificación la hace el PHP: el cliente siempre trabaja con texto plano. + +### 1. Listar idiomas activos +**Endpoint**: `/cms/lib/viewer_functions.php?action_ws=getLanguages` +**Método**: POST via `AcaiHttpClient.postViewerAction` +**Usado por**: `list_web_languages` +**Body**: `{}` +**Respuesta**: +```javascript +{ + success: true, + languages: [ + { name: string, label: string, prefix: string, urlPrefix: string, isDefault: boolean } + ], + defaultLanguage: string +} +``` +Notas: `prefix === "www"` es el idioma base (URLs sin prefijo, `urlPrefix === ""`, `isDefault === true`). Otros prefixes (p.ej. `"en"`) sirven bajo `//...`. + +### 2. Leer traducciones de registros +**Endpoint**: `/cms/lib/viewer_functions.php?action_ws=getTranslations` +**Método**: POST via `AcaiHttpClient.postViewerAction` +**Usado por**: `get_record_translations` +**Body**: +```javascript +{ + tableName: string, // sin prefijo cms_ + recordNums: number[], // PKs a leer + fields?: string[], // opcional: solo estos campos + prefix?: string // opcional: solo este idioma +} +``` +**Respuesta** (valores ya decodificados de Base64): +```javascript +{ + success: true, + tableName: string, + translations: { + "": { + "": { "": "" } + } + } +} +``` + +### 3. Escribir traducciones de un registro +**Endpoint**: `/cms/lib/viewer_functions.php?action_ws=setTranslations` +**Método**: POST via `AcaiHttpClient.postViewerAction` +**Usado por**: `set_record_translations` +**Body**: +```javascript +{ + tableName: string, // sin prefijo cms_ + recordNum: number, // PK a traducir + prefix: string, // idioma destino (nunca 'www') + fields: { "": "" } // '' borra la traducción +} +``` +**Respuesta**: `{ success: true, updated: number, deleted: number }` +**Errores**: `{ error: { message: string, code: string } }` — p.ej. si se intenta traducir `enlace` (prohibido: lo mantiene CocoEnlace). + +Notas: +- Solo campos traducibles: `textfield`, `textbox`, `wysiwyg`, `codigo`, `multitext` (el multitext se traduce como el JSON serializado completo en una fila). +- Un `fieldValue` vacío (`''`) borra la traducción y el runtime cae al idioma base. +- Vars de módulo: usar `tableName: 'builder_custom'` + el `recordNum`/`fieldName` que da `varsMeta` de `get_module_config_vars`. +- Lectura traducida en línea: las tools `list_table_records`/`get_record` pasan `options.translates = ` al `cmsApi` get (equivalente al header `X-ACAI-ACCEPT-LANGUAGE` en cms_api v3). + ## Patrones Comunes ### getApiClient Calls diff --git a/mcp-server/tools/index.js b/mcp-server/tools/index.js index 50a5132..e50f55a 100644 --- a/mcp-server/tools/index.js +++ b/mcp-server/tools/index.js @@ -11,6 +11,7 @@ import { registerHookTools } from './hooks/index.js'; import { registerLibrariesTools } from './libraries/index.js'; import { registerLayoutTools } from './layout/index.js'; import { registerDocsTools } from './docs/index.js'; +import { registerLanguageTools } from './languages/index.js'; /** * Register all tools on the MCP server @@ -29,4 +30,5 @@ export function registerTools(server) { registerLibrariesTools(server); registerLayoutTools(server); registerDocsTools(server); + registerLanguageTools(server); } diff --git a/mcp-server/tools/languages/getRecordTranslations.js b/mcp-server/tools/languages/getRecordTranslations.js new file mode 100644 index 0000000..63d9456 --- /dev/null +++ b/mcp-server/tools/languages/getRecordTranslations.js @@ -0,0 +1,74 @@ +import { z } from "zod"; +import { withAuth, getSessionCredentials } from "../../auth/index.js"; +import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js"; +import { AcaiHttpClient } from "../helpers/acaiHttpClient.js"; +import { withAuthParams } from "../helpers/authSchema.js"; + +export function registerGetRecordTranslationsTool(server) { + server.tool( + "get_record_translations", + `Read the stored translations for one or more records of a table, from the central cms_traducciones table. + +Translations are keyed by (prefix = language code, tableName without 'cms_', fieldName, recordNum). Values are stored Base64 in the DB but this tool returns them already decoded (plain text). An absent field/prefix means there is no translation and the base-language value is used at runtime. + +Params: +- tableName: table name WITHOUT the 'cms_' prefix (e.g. 'apartados', 'productos', 'builder_custom', 'textos_generales'). +- recordNums: array of record 'num' primary keys to read. +- fields (optional): only return these field names. Omit to return every translated field found. +- prefix (optional): only return translations for this language prefix (e.g. 'en'). Omit to return all languages. + +Returns translations shaped as { "": { "": { "": "" } } }. + +To translate module vars, first call get_module_config_vars to obtain varsMeta ({ fieldName, recordNum } per var) and read from tableName='builder_custom'. For template literals, read tableName='textos_generales' field 'texto'.`, + withAuthParams({ + tableName: z.string().describe("Table name without 'cms_' prefix (e.g. 'apartados', 'builder_custom', 'textos_generales')"), + recordNums: z.array(z.number()).describe("Array of record 'num' primary keys to read translations for"), + fields: z.array(z.string()).optional().describe("Only return these field names. Omit to return all translated fields."), + prefix: z.string().optional().describe("Only return translations for this language prefix (e.g. 'en'). Omit for all languages."), + }), + { readOnlyHint: true, destructiveHint: false }, + withAuth(async ({ tableName, recordNums, fields, prefix }, extra) => { + try { + const validationError = validateRequired( + { tableName, recordNums }, + ["tableName", "recordNums"], + "get_record_translations" + ); + if (validationError) return validationError; + + const credentials = await getSessionCredentials(extra.sessionId); + + const payload = { tableName, recordNums }; + if (fields && fields.length > 0) payload.fields = fields; + if (prefix) payload.prefix = prefix; + + const response = await AcaiHttpClient.postViewerAction( + credentials, + "getTranslations", + payload, + credentials.token, + credentials.tokenHash, + {}, + 15000 + ); + + const apiError = handleApiResponse(response.data, "get_record_translations"); + if (apiError) return apiError; + + return { + content: [{ + type: "text", + text: JSON.stringify({ + success: true, + action: "get_record_translations", + tableName: response.data?.tableName || tableName, + translations: response.data?.translations || {}, + }, null, 2) + }], + }; + } catch (error) { + return handleToolError(error, "get_record_translations", { tableName, recordNums }); + } + }) + ); +} diff --git a/mcp-server/tools/languages/index.js b/mcp-server/tools/languages/index.js new file mode 100644 index 0000000..4873960 --- /dev/null +++ b/mcp-server/tools/languages/index.js @@ -0,0 +1,9 @@ +import { registerListWebLanguagesTool } from './listWebLanguages.js'; +import { registerGetRecordTranslationsTool } from './getRecordTranslations.js'; +import { registerSetRecordTranslationsTool } from './setRecordTranslations.js'; + +export function registerLanguageTools(server) { + registerListWebLanguagesTool(server); + registerGetRecordTranslationsTool(server); + registerSetRecordTranslationsTool(server); +} diff --git a/mcp-server/tools/languages/listWebLanguages.js b/mcp-server/tools/languages/listWebLanguages.js new file mode 100644 index 0000000..7453bbe --- /dev/null +++ b/mcp-server/tools/languages/listWebLanguages.js @@ -0,0 +1,54 @@ +import { withAuth, getSessionCredentials } from "../../auth/index.js"; +import { handleToolError, handleApiResponse } from "../helpers/errorHandler.js"; +import { AcaiHttpClient } from "../helpers/acaiHttpClient.js"; +import { withAuthParams } from "../helpers/authSchema.js"; + +export function registerListWebLanguagesTool(server) { + server.tool( + "list_web_languages", + `List the active languages configured for the current website (from settings.dat.php [idiomas]). + +Returns an array of languages, each with: +- name: internal code (e.g. 'espanol') +- label: human label +- prefix: the translation prefix stored in cms_traducciones. The prefix "www" is the BASE language (its URLs have NO path prefix). Any other prefix (e.g. "en") is a secondary language served under //... +- urlPrefix: the URL path segment for that language ('' for the base language, e.g. 'en' otherwise) +- isDefault: true for the base language + +Also returns defaultLanguage. Use the prefix values here when calling get_record_translations / set_record_translations.`, + withAuthParams({}), + { readOnlyHint: true, destructiveHint: false }, + withAuth(async (_args, extra) => { + try { + const credentials = await getSessionCredentials(extra.sessionId); + + const response = await AcaiHttpClient.postViewerAction( + credentials, + "getLanguages", + {}, + credentials.token, + credentials.tokenHash, + {}, + 15000 + ); + + const apiError = handleApiResponse(response.data, "list_web_languages"); + if (apiError) return apiError; + + return { + content: [{ + type: "text", + text: JSON.stringify({ + success: true, + action: "list_web_languages", + languages: response.data?.languages || [], + defaultLanguage: response.data?.defaultLanguage, + }, null, 2) + }], + }; + } catch (error) { + return handleToolError(error, "list_web_languages"); + } + }) + ); +} diff --git a/mcp-server/tools/languages/setRecordTranslations.js b/mcp-server/tools/languages/setRecordTranslations.js new file mode 100644 index 0000000..eb4f392 --- /dev/null +++ b/mcp-server/tools/languages/setRecordTranslations.js @@ -0,0 +1,79 @@ +import { z } from "zod"; +import { withAuth, getSessionCredentials } from "../../auth/index.js"; +import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js"; +import { AcaiHttpClient } from "../helpers/acaiHttpClient.js"; +import { withAuthParams } from "../helpers/authSchema.js"; + +export function registerSetRecordTranslationsTool(server) { + server.tool( + "set_record_translations", + `Write translations for a single record + language into the central cms_traducciones table. Values are stored Base64 automatically (transparent — pass plain text). Only translatable field types can be translated: textfield, textbox, wysiwyg, codigo and multitext. + +Four usage scenarios: +1. Normal records — translate the visible text fields of any content table. tableName without 'cms_', recordNum = the record 'num', fields = { fieldName: translatedText }. +2. configuracion / configuracion_tienda — the global settings tables are ordinary records; translate their text fields the same way (locate the row with list_table_records). +3. textos_generales — the template literals used by the Twig '| translate' filter are normal records: translate their 'texto' field. Find the right num by 'identificador' with list_table_records first (tableName='textos_generales', fields={ texto: '...' }). +4. Module vars — the textual values of a module live in the 'builder_custom' table. Call get_module_config_vars to get varsMeta ({ fieldName, recordNum } per var, and per item for multi vars), then set tableName='builder_custom', recordNum + fields={ : translatedText } from varsMeta. + +Rules: +- An EMPTY STRING ('') as a value DELETES that translation (falls back to base language at runtime). +- The 'enlace' field is NEVER translated by hand — the CocoEnlace engine maintains it; the PHP rejects it. +- multitext fields are translated as the full serialized JSON string in a single row (translate the whole JSON value, not individual items). +- prefix is the language code from list_web_languages (e.g. 'en'); do NOT write to prefix 'www' (base language is edited via normal record fields, not translations). + +Params: +- tableName (string) without 'cms_' prefix. +- recordNum (number) the record 'num' primary key. +- prefix (string) target language prefix (e.g. 'en'). +- fields (object) { fieldName: translatedValue } map. '' deletes.`, + withAuthParams({ + tableName: z.string().describe("Table name without 'cms_' prefix (e.g. 'apartados', 'builder_custom', 'textos_generales')"), + recordNum: z.number().describe("Record 'num' primary key to translate"), + prefix: z.string().describe("Target language prefix from list_web_languages (e.g. 'en'). Never 'www' (base language)."), + fields: z.record(z.string()).describe("Map { fieldName: translatedValue }. Empty string '' deletes the translation. 'enlace' is forbidden."), + }), + { readOnlyHint: false, destructiveHint: false }, + withAuth(async ({ tableName, recordNum, prefix, fields }, extra) => { + try { + const validationError = validateRequired( + { tableName, recordNum, prefix, fields }, + ["tableName", "recordNum", "prefix", "fields"], + "set_record_translations" + ); + if (validationError) return validationError; + + const credentials = await getSessionCredentials(extra.sessionId); + + const response = await AcaiHttpClient.postViewerAction( + credentials, + "setTranslations", + { tableName, recordNum, prefix, fields }, + credentials.token, + credentials.tokenHash, + {}, + 15000 + ); + + const apiError = handleApiResponse(response.data, "set_record_translations"); + if (apiError) return apiError; + + return { + content: [{ + type: "text", + text: JSON.stringify({ + success: true, + action: "set_record_translations", + tableName, + recordNum, + prefix, + updated: response.data?.updated, + deleted: response.data?.deleted, + }, null, 2) + }], + }; + } catch (error) { + return handleToolError(error, "set_record_translations", { tableName, recordNum, prefix }); + } + }) + ); +} diff --git a/mcp-server/tools/records/getRecord.js b/mcp-server/tools/records/getRecord.js index dc3a345..31cd2aa 100644 --- a/mcp-server/tools/records/getRecord.js +++ b/mcp-server/tools/records/getRecord.js @@ -20,9 +20,10 @@ Examples: recordNum: z.string().describe("Record 'num' (primary key)"), loadUploads: z.boolean().optional().default(true).describe("Load upload field data (default: true)"), loadRelations: z.boolean().optional().default(true).describe("Resolve foreign key relations (default: true)"), + lang: z.string().optional().describe("Language prefix (e.g. 'en'). Returns translated values for translatable fields"), }), { readOnlyHint: true, destructiveHint: false }, - withAuth(async ({ tableName, recordNum, loadUploads = true, loadRelations = true }, extra) => { + withAuth(async ({ tableName, recordNum, loadUploads = true, loadRelations = true, lang }, extra) => { try { const validationError = validateRequired( { tableName, recordNum }, @@ -53,6 +54,11 @@ Examples: payload.options.ignoreSchema = true; } + // Si se pide un idioma, el motor CocoDB aplica la traduccion en lectura. + if (lang) { + payload.options.translates = lang; + } + const response = await AcaiHttpClient.postCmsApi( credentials, "get", diff --git a/mcp-server/tools/records/list.js b/mcp-server/tools/records/list.js index 2e0eb92..8f48a35 100644 --- a/mcp-server/tools/records/list.js +++ b/mcp-server/tools/records/list.js @@ -15,9 +15,10 @@ export function registerListTableRecordsTool(server) { limit: z.number().optional().describe("Max records to return. Default: 50. Use 5-10 for previews, up to 200 max for large exports."), fields: z.array(z.string()).optional().describe("Return only these columns (e.g., ['num', 'titulo', 'precio']). Omit to return all columns. Always include 'num' if you need record IDs."), truncateText: z.number().optional().describe("Truncate string field values longer than this many chars. Appends '... [truncated, N chars]'. Combine with 'fields' for maximum token savings."), + lang: z.string().optional().describe("Language prefix (e.g. 'en'). Returns translated values for translatable fields"), }), { readOnlyHint: true, destructiveHint: false }, - withAuth(async ({ tableName, page, where, limit, fields, truncateText }, extra) => { + withAuth(async ({ tableName, page, where, limit, fields, truncateText, lang }, extra) => { try { // Validate required parameters const validationError = validateRequired({ tableName }, ['tableName'], 'list_table_records'); @@ -33,6 +34,11 @@ export function registerListTableRecordsTool(server) { options: {} }; + // Si se pide un idioma, el motor CocoDB aplica la traduccion en lectura. + if (lang) { + payload.options.translates = lang; + } + // Send to CMS API via viewer_functions const response = await AcaiHttpClient.postCmsApi( credentials,