Compare commits

..

3 Commits

Author SHA1 Message Date
Jordan Diaz
9c3d9fb999 fix(mcp): las traducciones de vars de modulo van por NOMBRE de var, no columna fisica
Una sesion real del agente guardo title3/title6/title2 (columnas de
builder_custom segun varsMeta.fieldName, como decian las docs) y el front
no pintaba nada: el runtime traduce vars con t($record, $var) por nombre
de var. Corregidas descripciones de set/get_record_translations, docs
03/09/11b y ACAI_ENDPOINTS; ademas el puente PHP ahora rechaza titleN/
textN sobre builder_custom con error explicativo (guardrail).
2026-07-17 08:18:18 +00:00
Jordan Diaz
76a63ce4e4 docs(mcp): enlace traducible por idioma en set_record_translations
Path absoluto con prefijo (/en/...); CocoEnlace regenera las filas al
cambiar el enlace base. Actualizados tool description, ACAI_ENDPOINTS y
docs 04/09/11b.
2026-07-16 20:56:03 +00:00
Jordan Diaz
d46c204ed0 feat(mcp): tools de idiomas y traducciones
- Nueva categoria tools/languages: list_web_languages,
  get_record_translations y set_record_translations (registros, config,
  textos_generales y vars de modulo via builder_custom).
- Param lang opcional en list_table_records y get_record
  (options.translates de CocoDB).
- Docs actualizados (03/04/06/09/11b + ACAI_ENDPOINTS) con el modelo de
  cms_traducciones y los workflows de traduccion.
2026-07-16 20:09:47 +00:00
13 changed files with 382 additions and 3 deletions

View File

@@ -203,6 +203,16 @@ Acceso en Twig:
Las variables son **propiedades del objeto iterado**, no variables sueltas. 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`: de ahí interesa el `recordNum` (la fila de `builder_custom` de la instancia; en vars multi, uno por item).
2. `set_record_translations({ tableName: "builder_custom", recordNum: <de varsMeta>, prefix, fields: { <NOMBRE de la var>: "texto traducido" } })` — p.ej. `{ titulo: "...", subtitulo: "..." }`.
CRÍTICO: la clave de traducción es el NOMBRE de la var del builder.json (`titulo`, `subtitulo`, `enlace_anchor`...), NUNCA la columna física de `builder_custom` (`title3`, `text1`...). El runtime traduce con `t($record, $var)` por nombre de var; con la columna física se guarda pero no se pinta jamás (la action lo rechaza con 400). Tampoco añadas filtros `| translate` a las plantillas del módulo: el motor traduce las vars automáticamente. Ver el workflow completo en `09-mcp-tools-reference.md`.
## Layout global vs módulos ## 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`. `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`.

View File

@@ -152,6 +152,20 @@ create_or_update_record:
metatag_descripcion: "Descubre nuestros servicios…" 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: "<prefix>"` 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`.
### El `enlace` traducido lo define el usuario
El `enlace` traducido es editable con `set_record_translations` (formato: path absoluto con prefijo de idioma, p.ej. `/en/contact/` para la página base `/contacto/`). Solo hazlo si el usuario lo pide; ten en cuenta que al cambiar el `enlace` base, CocoEnlace regenera automáticamente las filas de enlace por idioma (formato `/<prefix><enlaceBase>`) y puede pisar la personalización.
## Patrón canónico — Detalle de registro ## 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. 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.

View File

@@ -149,7 +149,7 @@ $datos = CmsApi::get("productos", "", "", "", [
| `uploads` | bool | `true` | Incluir datos de upload fields | | `uploads` | bool | `true` | Incluir datos de upload fields |
| `relations` | bool/array | `true` | Resolver foreign keys. Array para limitar: `['categoria']` | | `relations` | bool/array | `true` | Resolver foreign keys. Array para limitar: `['categoria']` |
| `relationsDepth` | int | 2 | Profundidad de relaciones anidadas | | `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 | | `groupBy` | string | null | Cláusula GROUP BY |
| `aggregates` | array | `[]` | Funciones de agregación | | `aggregates` | array | `[]` | Funciones de agregación |
| `onlyFields` | array | null | Seleccionar solo ciertos campos | | `onlyFields` | array | null | Seleccionar solo ciertos campos |
@@ -157,6 +157,8 @@ $datos = CmsApi::get("productos", "", "", "", [
| `redis` | bool | null | Forzar cache Redis | | `redis` | bool | null | Forzar cache Redis |
| `redis_expire` | int | 60 | TTL del cache (segundos) | | `redis_expire` | int | 60 | TTL del cache (segundos) |
**Idioma en lectura**: pasar `options['translates'] => '<prefix>'` 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: <prefix>`, 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()` ### Insert — `CmsApi::insert()`
```php ```php

View File

@@ -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) | | `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`: path absoluto con prefijo (`/en/...`); CocoEnlace lo regenera si cambia el enlace base |
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 ### Documentación
| Tool | Acció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). - 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"`. - 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 })` — de `varsMeta` toma el `recordNum` (fila de `builder_custom`; en vars multi, uno por item).
3. `set_record_translations({ tableName: "builder_custom", recordNum: <de varsMeta>, prefix, fields: { <NOMBRE de la var>: "..." } })` — la clave es el nombre de la var (`titulo`, `subtitulo`, `enlace_anchor`), NUNCA la columna física (`title3`): el motor traduce por nombre de var y la columna física se rechaza con 400. No añadas `| translate` a la plantilla: las vars se traducen solas.
4. Para vars multi, repite por cada item con su `recordNum` de `varsMeta` (las claves siguen siendo los nombres de las sub-vars).
### 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: <num>, prefix, fields: { texto: "traducción" } })`.
## Reglas globales para todas las tools ## Reglas globales para todas las tools
1. **`tableName` siempre SIN prefijo `cms_`** (excepto en `queryDB` Twig y en el `middleWare` de `set_hook_middleware`). 1. **`tableName` siempre SIN prefijo `cms_`** (excepto en `queryDB` Twig y en el `middleWare` de `set_hook_middleware`).

View File

@@ -87,6 +87,20 @@ Resumen ejecutable de reglas críticas, tipos de campo, filtros y formatos de da
| `multitext` | String JSON | `"[{\"item\":\"valor\"}]"` | | `multitext` | String JSON | `"[{\"item\":\"valor\"}]"` |
| `upload` | NO enviar — usar `upload_record_image` después | | `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` |
| Traducir `enlace` con cuidado | Es editable por idioma (path absoluto, p.ej. `/en/contact/`); si cambia el enlace base, CocoEnlace lo regenera |
| 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: "<prefix>"` |
| Vars de módulo | Sobre `builder_custom` con el `recordNum` de `varsMeta`, clave = NOMBRE de la var (`titulo`), nunca la columna `titleN` |
| Textos generales | Traducir el campo `texto` del registro en `textos_generales` (localiza por `identificador`) |
## Variables globales en Twig ## Variables globales en Twig
| Variable | Descripción | | Variable | Descripción |

View File

@@ -323,6 +323,75 @@ action=uploadModify
**Usado por**: `delete_record_upload` **Usado por**: `delete_record_upload`
**Headers**: `X-Acai-Token`, `X-Requested-With: XMLHttpRequest` **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 `/<urlPrefix>/...`.
### 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: {
"<recordNum>": {
"<prefix>": { "<fieldName>": "<valor>" }
}
}
}
```
### 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: { "<fieldName>": "<valor>" } // '' borra la traducción
}
```
**Respuesta**: `{ success: true, updated: number, deleted: number }`
**Errores**: `{ error: { message: string, code: string } }` — p.ej. campos de sistema (`num`, `builder`, `controlador`). `enlace` SÍ es traducible: debe ser un path absoluto (se normaliza la barra inicial); al cambiar el enlace base, CocoEnlace regenera las traducciones.
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: `tableName: 'builder_custom'` + `recordNum` de `varsMeta` (`get_module_config_vars`), pero el fieldName es el NOMBRE DE LA VAR del builder.json (`titulo`, `subtitulo`, `enlace_anchor`...), nunca la columna física `titleN`/`textN` (la action la rechaza con 400).
- Lectura traducida en línea: las tools `list_table_records`/`get_record` pasan `options.translates = <prefix>` al `cmsApi` get (equivalente al header `X-ACAI-ACCEPT-LANGUAGE` en cms_api v3).
## Patrones Comunes ## Patrones Comunes
### getApiClient Calls ### getApiClient Calls

View File

@@ -11,6 +11,7 @@ import { registerHookTools } from './hooks/index.js';
import { registerLibrariesTools } from './libraries/index.js'; import { registerLibrariesTools } from './libraries/index.js';
import { registerLayoutTools } from './layout/index.js'; import { registerLayoutTools } from './layout/index.js';
import { registerDocsTools } from './docs/index.js'; import { registerDocsTools } from './docs/index.js';
import { registerLanguageTools } from './languages/index.js';
/** /**
* Register all tools on the MCP server * Register all tools on the MCP server
@@ -29,4 +30,5 @@ export function registerTools(server) {
registerLibrariesTools(server); registerLibrariesTools(server);
registerLayoutTools(server); registerLayoutTools(server);
registerDocsTools(server); registerDocsTools(server);
registerLanguageTools(server);
} }

View File

@@ -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 { "<recordNum>": { "<prefix>": { "<fieldName>": "<value>" } } }.
To read module var translations, call get_module_config_vars for the recordNum (varsMeta) and read tableName='builder_custom' — rows are keyed by VAR NAME ('titulo', 'subtitulo'...), not by physical column (title3). 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 });
}
})
);
}

View File

@@ -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);
}

View File

@@ -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>/...
- 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");
}
})
);
}

View File

@@ -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 and use its recordNum, BUT the field key MUST be the VAR NAME from builder.json (e.g. 'titulo', 'subtitulo', 'enlace_anchor') — NEVER the physical column (title3, text1...): the engine translates module vars by var name and physical columns are rejected. Example: set tableName='builder_custom', recordNum=<varsMeta recordNum>, fields={ titulo: 'Translated title' }.
Rules:
- An EMPTY STRING ('') as a value DELETES that translation (falls back to base language at runtime).
- 'enlace' IS translatable: absolute path with the language prefix (e.g. '/en/contact/'). Note: when the base 'enlace' changes, CocoEnlace regenerates per-language rows and may overwrite 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' must be an absolute path (e.g. '/en/contact/')."),
}),
{ 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 });
}
})
);
}

View File

@@ -20,9 +20,10 @@ Examples:
recordNum: z.string().describe("Record 'num' (primary key)"), recordNum: z.string().describe("Record 'num' (primary key)"),
loadUploads: z.boolean().optional().default(true).describe("Load upload field data (default: true)"), 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)"), 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 }, { readOnlyHint: true, destructiveHint: false },
withAuth(async ({ tableName, recordNum, loadUploads = true, loadRelations = true }, extra) => { withAuth(async ({ tableName, recordNum, loadUploads = true, loadRelations = true, lang }, extra) => {
try { try {
const validationError = validateRequired( const validationError = validateRequired(
{ tableName, recordNum }, { tableName, recordNum },
@@ -53,6 +54,11 @@ Examples:
payload.options.ignoreSchema = true; 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( const response = await AcaiHttpClient.postCmsApi(
credentials, credentials,
"get", "get",

View File

@@ -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."), 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."), 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."), 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 }, { readOnlyHint: true, destructiveHint: false },
withAuth(async ({ tableName, page, where, limit, fields, truncateText }, extra) => { withAuth(async ({ tableName, page, where, limit, fields, truncateText, lang }, extra) => {
try { try {
// Validate required parameters // Validate required parameters
const validationError = validateRequired({ tableName }, ['tableName'], 'list_table_records'); const validationError = validateRequired({ tableName }, ['tableName'], 'list_table_records');
@@ -33,6 +34,11 @@ export function registerListTableRecordsTool(server) {
options: {} 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 // Send to CMS API via viewer_functions
const response = await AcaiHttpClient.postCmsApi( const response = await AcaiHttpClient.postCmsApi(
credentials, credentials,