Integraciones

Sanity (TMS)

El contenido se traduce automáticamente del inglés por Phrase Language AI.

El plugin proporciona acceso al contenido traducido de Phrase desde dentro del estudio de Sanity.

Solo se admiten traducciones a nivel de documento. No se admiten traducciones a nivel de campo.

Características:

  • Vistas previas en tiempo real

    Las traducciones se mantienen sincronizadas para permitir que los lingüistas y traductores vean los cambios de vista previa en tiempo real.

  • Re-traducciones inteligentes

    El plugin compara qué contenido ha cambiado desde la última traducción y solo envía esos cambios a Phrase.

  • Traducción automática de referencias

    Al emitir traducciones, los editores pueden elegir traducir también documentos referenciados por el actual y el plugin los vinculará automáticamente por idioma objetivo.

  • Esquemas flexibles

    No importa la estructura, el plugin se adapta a ella y asegura que el contenido traducido final esté de acuerdo con los esquemas de Sanity.

  • Flujos de trabajo de Phrase

    Los flujos de trabajo de traducción en Phrase permanecen iguales; no se requiere reentrenar o reconfigurar operaciones.

Instalación del Plugin de Sanity

La instalación se realiza en la línea de comandos.

Se asume que un generador de sitios web y Sanity Studio ya están configurados. Si no, utiliza una de las plantillas de inicio proporcionadas por Sanity.

Instalación

Navega al proyecto que contiene la instancia de Sanity Studio e instala el plugin:

npm install sanity-plugin-phrase

# o pnpm, yarn, bun

Variables de entorno

Antes de configurar el plugin, se deben establecer las siguientes variables de entorno. Crea un archivo `.env` (o `.env.local` para Next.js) en la raíz del proyecto.

Los ejemplos a continuación se dan para NextJS. Para otros frameworks, consulta su documentación específica, y ten en cuenta que el prefijo `NEXT_PUBLIC_` puede necesitar ser eliminado para variables públicas.

Importante

Las variables del lado del servidor (SANITY_WRITE_TOKEN, PHRASE_USER_NAME, PHRASE_PASSWORD) nunca deben ser expuestas al cliente. En Next.js, solo las variables con el prefijo NEXT_PUBLIC_ son expuestas al navegador.

# URL base de tu sitio (usada para enlaces de vista previa)
NEXT_PUBLIC_BASE_URL="http://localhost:3000"

# URL donde se ubicará el manejador del backend del plugin
NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT="http://localhost:3000/api/phrase"

# Región del centro de datos de Phrase ('eu' o 'us')
NEXT_PUBLIC_PHRASE_REGION="eu"

# Configuración del proyecto de Sanity
NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
NEXT_PUBLIC_SANITY_DATASET="production"

# Token de API de Sanity con permisos de escritura (solo del lado del servidor)
SANITY_WRITE_TOKEN=""

# Credenciales de Phrase (solo del lado del servidor)
# Nota: La API de Phrase espera solo la parte del nombre de usuario, NO la dirección de correo electrónico completa
PHRASE_USER_NAME="phraseUsername"
PHRASE_PASSWORD="secretPassword"

Configuración del plugin

El plugin se agrega a sanity.config.ts con las opciones de configuración requeridas:

// sanity.config.ts
import { defineConfig } from 'sanity'
import {
  phrasePlugin,
  definePhraseOptions,
  documentInternationalizationAdapter,
} from 'sanity-plugin-phrase'

const PHRASE_CONFIG = definePhraseOptions({
  // Requerido: adaptador i18n para la internacionalización de documentos
  i18nAdapter: documentInternationalizationAdapter(),

  // Requerido: Tipos de documentos que se pueden traducir
  translatableTypes: ['page', 'post', 'article'],

  // Requerido: Idioma de origen (idioma principal)
  // Esto debe coincidir con el idioma definido en la plantilla de tu proyecto Phrase
  sourceLang: 'en',

  // Requerido: Idiomas de destino a los que los usuarios pueden traducir
  // Usa los mismos códigos que tus documentos de Sanity
  // Esta lista debe coincidir con los idiomas definidos en la plantilla del proyecto Phrase
  idiomasObjetivoSoportados: ['es', 'fr', 'de', 'pt'],

  // Requerido: La URL de tu punto final de API de backend
  apiEndpoint: process.env.NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT!,

  // Requerido: Región del centro de datos de Phrase ('eu' o 'us')
  phraseRegion: process.env.NEXT_PUBLIC_PHRASE_REGION como 'eu' | 'us',

  // Requerido: Plantillas de proyecto de Phrase disponibles para editores
  phraseTemplates: [
    {
      templateUid: 'YOUR_TEMPLATE_UID_HERE',
      etiqueta: 'Plantilla de Traducción Predeterminada',
    },
  ],

  // Requerido: Generar URLs de vista previa para lingüistas
  getDocumentPreview: (doc, sanityClient) => {
    const publishedId = doc._id.replace('drafts.', '')
    return `${process.env.NEXT_PUBLIC_BASE_URL}/api/draft?id=${publishedId}`
  },

  // Configuraciones opcionales

  // Profundidad máxima para traducir documentos referenciados (predeterminado: 3)
  maxReferencesDepth: 3,

  // Permitir la traducción de documentos borrador (predeterminado: falso)
  translateDrafts: false,

  // Transformadores de datos personalizados para tipos de contenido especiales
  dataTransformers: [],

  // Configuración de registro para depuración
  logger: {
    minimumLogLevel: 'info', // 'debug' | 'info' | 'warning' | 'error' | 'fatal'
  },

  // Ocultar el tablero de Phrase según los roles de usuario
  isPhraseDashboardHidden: (context) =>
    !(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})

export default defineConfig({
  // ... tu configuración existente
  plugins: [
    phrasePlugin(PHRASE_CONFIG),
    // ... otros plugins
  ],
})// sanity.config.(js|ts)
import {
  phrasePlugin,
  documentInternationalizationAdapter,
} from 'sanity-plugin-phrase'

const PHRASE_CONFIG = definePhraseOptions({
  /**
   * El adaptador i18n a utilizar para este plugin.
   * Será responsable de obtener y modificar documentos para cada idioma objetivo.
   *
   * Consulta a continuación para más información sobre adaptadores.
   */
  i18nAdapter: documentInternationalizationAdapter(),

  /**
   * Tipos de esquema de Sanity que el plugin puede traducir
   */
  translatableTypes: ['page', 'post', 'course', 'lesson', 'definition'], 

  /**
   * Código de idioma de todos los idiomas a los que los usuarios pueden traducir.
   * Debe ser el mismo que el almacenado en sus documentos de Sanity y utilizado por su front-end. El plugin lo traducirá automáticamente al formato de Phrase.
   */
  supportedTargetLangs: ['cz', 'es', 'pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru'],

  /**
   * Código de idioma de la lengua fuente que será traducida.
   * Debe ser el mismo que el almacenado en sus documentos de Sanity y utilizado por su front-end. El plugin lo traducirá automáticamente al formato de Phrase.
   */
  sourceLang: 'en',

  /**
   * Según lo definido por la configuración de su cuenta de Phrase
   * Ya sea `eu` o `us`
   */
  phraseRegion: 'us|eu',

  /**
   * La URL de su API de backend de plugin configurada.
   *
   * **Nota:** siga los pasos para configurar el endpoint, descritos a continuación
   */
  apiEndpoint: 'https://my-site.com/api/phrase',

  /**
   * Utilizado para redirigir a los lingüistas desde el tablero de Phrase a la vista previa del front-end de sus traducciones.
   */
  getDocumentPreview: async (doc, sanityClient) => {
    const publishedId = doc._id.replace('drafts.', '')
    return `${process.env.NEXT_PUBLIC_FRONT_END_URL}/api/draft?publishedId=${publishedId}`
  },

  /**
   * Plantillas de proyecto de Phrase que sus editores pueden usar al solicitar traducciones.
   *
   * **Nota:** siga los pasos para establecer plantillas, descritos a continuación
   */
  phraseTemplates: [
    {
      templateUid: '1jYg0Pc1d8kAHUyM0tgdmt',
      etiqueta: '[Sanity.io] Plantilla predeterminada',
    },
  ],

  /**
   * @opcional
   * En caso de que desee mostrar u ocultar el tablero de Phrase según los privilegios del usuario.
   *
   * Recibe un contexto con el usuario actual y el documento y debe devolver un booleano.
   */
  isPhraseDashboardHidden: (context) =>
    !(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})

export default defineConfig({
  // ...
  plugins: [
    // ...
    phrasePlugin(PHRASE_CONFIG),
  ],
})

Configuración del Plugin de Sanity

Inyección de esquema

Para indicar al plugin qué tipos de documentos se pueden traducir, pasa un array de tipos de documentos a la injectPhraseIntoSchema función en el archivo sanity.config.ts:

// sanity.config.ts
importar { injectPhraseIntoSchema } de 'sanity-plugin-phrase'

// Lista de tipos de esquema traducibles. Generalmente exportado de un archivo índice
// donde sea que tengas tu esquema de Sanity
const ESQUEMAS_TRADUCIBLES = ['pagina', 'publicación', 'curso', 'lección', 'definición']

export default defineConfig({
  esquema: {
    tipos: injectPhraseIntoSchema(ESQUEMAS_TRADUCIBLES, CONFIGURACIÓN_FRASE),
    plantillas: (prev) =>
      prev.filter((plantilla) => !ESQUEMAS_TRADUCIBLES.includes(plantilla.id)),
  },
  plugins: [
    // ...
    phrasePlugin({
      // Tus opciones de configuración aquí
    }),
  ],
})

Excluyendo PTDs de listas de documentos

PTDs (Documentos de Traducción de Frases) son documentos temporales que no deberían aparecer en listas de documentos normales dentro de Sanity Studio. La constante NOT_PTD proporciona un filtro GROQ para este propósito:

// sanity.config.ts
importar { NOT_PTD } de 'sanity-plugin-phrase/utils'

export default defineConfig({
  // ... otra configuración
  plugins: [
    structureTool({
      estructura: (S) =>
        S.lista()
          .title('Content')
          .items([
            S.listItem()
              .title('Posts')
              .schemaType('post')
              .child(
                S.documentList()
                  .title('Posts')
                  .filter(`_type == "post" && ${NOT_PTD}`),
              ),
            // ... otros elementos
          ]),
    }),
  ],
})

Ocultando el menú de traducción de PTDs

Al usar el plugin de internacionalización de documentos, el menú de traducción debe estar oculto para los PTDs. La esPtdId utilidad identifica documentos PTD:

importar { esPtdId } de 'sanity-plugin-phrase/utils'
importar { MenuInternacionalizaciónDocumento } de '@sanity/document-internationalization'

// Usa el mismo array que translatableTypes de tu PHRASE_CONFIG
const TIPOS_TRADUCIBLES = ['página', 'publicación', 'artículo']

export default defineConfig({
  documento: {
    unstable_languageFilter: (prev, ctx) => {
      const { tipoEsquema, idDocumento } = ctx

      // Solo mostrar menú de traducción para documentos reales, no PTDs
      return TRANSLATABLE_TYPES.includes(schemaType) &&
        documentId &&
        !isPtdId(documentId)
        ? [...prev, DocumentInternationalizationMenu]
        : prev
    },
  },
})

Configuración de Phrase

El plugin no tiene configuración en la interfaz de Phrase, pero Phrase debe configurarse para enviar notificaciones de webhook al punto final de la API del backend. Esto permite actualizaciones en tiempo real a medida que avanza la traducción.

Crear un webhook

Crear un webhook con estas configuraciones:

  • URL

    La URL del punto final de la API del plugin, según lo configurado en la opción apiEndpoint.

  • Eventos:

    • Trabajos

      • Trabajo eliminado

      • Job assigned

      • La fecha de entrega del Trabajo ha cambiado

      • Meta del trabajo actualizada

    • Proyectos

      • Proyecto eliminado

      • La fecha de entrega del Proyecto ha cambiado

    • Otro

      • Pre-traducción terminada

Esto asegura que el plugin sea notificado de cualquier cambio en los proyectos de Phrase y puede mantener los datos de Sanity sincronizados.

Configurar plantilla(s) de proyectos

Configurar plantilla(s) de proyecto de Phrase con las propiedades requeridas para flujos de trabajo y requisitos del equipo. Se puede ofrecer una o más plantillas para elegir al solicitar una nueva traducción. Las plantillas de proyecto de Phrase deben tener configuraciones específicas de importación JSON para que el plugin funcione correctamente. Estas configuraciones controlan qué campos se envían a los traductores y cuáles se preservan como metadatos.

Importación de archivo JSON

Usar regex para excluir claves específicas:

(^|.*\/)
(_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|TUS_CLAVES_IGNORADAS_AQUÍ|
(_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|TUS_CLAVES_IGNORADAS_AQUÍ)
/.*)

Esta expresión incluye claves duplicadas a propósito para asegurar que sean ignoradas por el analizador RegEx de Phrase. Asegúrate de que estén correctamente duplicadas.

  • Excluye datos específicos de localización, como el idioma de un documento dado si usas @sanity/document-internationalization.

  • Incluye cualquier clave específica del proyecto que no requiera traducción, como un slug para contenido que use la misma ruta en todos los idiomas. Reemplaza TUS_CLAVES_IGNORADAS_AQUÍ con una lista de claves separadas por tuberías para ignorar.

  • Context note: /_sanityContext

Idioma fuente

Actualmente, este plugin opera bajo la suposición de tener un único idioma fuente. La(s) plantilla(s) del proyecto deben tener la misma fuente que la configurada en sourceLanguage del plugin.

Idiomas de destino

Asegúrate de que los idiomas elegidos en Phrase estén sincronizados con lo que hay en la configuración del plugin.

Sanity apiEndpoint

Este es el endpoint que el plugin utiliza para comunicarse con el Sanity Studio. Se utiliza para autenticar en la API de Phrase, recibir webhooks y solicitudes de usuario desde el estudio de Sanity.

Crea un endpoint API personalizado en el proyecto de Sanity para manejar estas solicitudes. Una de las formas más fáciles de hacer esto es usar funciones sin servidor a través de frameworks de front-end como NextJS, Remix, SvelteKit o Nuxt.

Accede a configurar el manejador con un patrón de Solicitud-Respuesta a través de import {createRequestHandler} de sanity-plugin-phrase/backend o usa el manejador interno directamente a través de import {createInternalHandler} de sanity-plugin-phrase/backend. Asegúrate de que las solicitudes CORS se manejen correctamente, el estudio y el endpoint tienen orígenes diferentes.

El directorio de la aplicación de NextJS no es compatible actualmente, ya que analiza incorrectamente el controlador de backend como un componente cliente de React.

Este ejemplo demuestra cómo crear un Controlador de Ruta en la ruta configurada apiEndpoint en /api/phrase utilizando el enrutador de Páginas de Next.js:

// app/api/phrase/route.ts
// Soporte de ruta API de Next.js: https://nextjs.org/docs/api-routes/introduction
importar tipo { NextApiRequest, NextApiResponse } de 'next'
importar { PHRASE_CONFIG } de 'phraseConfig'
importar { createInternalHandler } de 'sanity-plugin-phrase/backend'

importar { writeToken } de '~/lib/sanity.api'
importar { client } de '~/lib/sanity.client'

exportar const maxDuration = 60
exportar const dynamic = 'force-dynamic'

const phraseHandler = createInternalHandler({
  phraseCredentials: {
    userName: process.env.PHRASE_USER_NAME || '',
    password: process.env.PHRASE_PASSWORD || '',
  },
  sanityClient: client.withConfig({ token: writeToken }),
  opcionesDelPlugin: PHRASE_CONFIG,
})

exportar default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  res.setHeader('Access-Control-Allow-Origin', '*')
  res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
  res.setHeader('Access-Control-Allow-Headers', '*')

  if (req.method?.toUpperCase() === 'OPTIONS') {
    res.status(200).json({})
    return
  }

  if (
    !req.method ||
    (req.method.toUpperCase() !== 'POST' && req.method.toUpperCase() !== 'GET')
  ) {
    res.status(405).json({ error: 'Método no permitido' })
    return
  }

  const phraseRes = await phraseHandler(
    req.method.toUpperCase() === 'POST' ? req.body : req.query,
  )
  const resBody = await phraseRes.json().catch(() => {})

  Array.from(phraseRes.headers.entries()).forEach((value: [string, any]) => {
    res.setHeader(value[0], value[1])
  })
  res.status(phraseRes.status).json(resBody)
}// src/pages/api/phrase.ts
// Ruta API de Next.js: https://nextjs.org/docs/pages/building-your-application/routing/api-routes
importar tipo { NextApiRequest, NextApiResponse } de 'next'
importar { PHRASE_CONFIG } de 'phraseConfig'
importar { createInternalHandler } de 'sanity-plugin-phrase/backend'
importar { writeToken } de '~/lib/sanity.api'
importar { client } de '~/lib/sanity.client'

const phraseHandler = createInternalHandler({
  phraseCredentials: {
    userName: process.env.PHRASE_USER_NAME || '',
    password: process.env.PHRASE_PASSWORD || '',
  },
  sanityClient: client.withConfig({ token: writeToken }),
  opcionesDelPlugin: PHRASE_CONFIG,
})

exportar default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  res.setHeader('Access-Control-Allow-Origin', '*')
  res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
  res.setHeader('Access-Control-Allow-Headers', '*')

  if (req.method?.toUpperCase() === 'OPTIONS') {
    res.status(200).json({})
    return
  }

  if (
    !req.method ||
    (req.method.toUpperCase() !== 'POST' && req.method.toUpperCase() !== 'GET')
  ) {
    res.status(405).json({ error: 'Método no permitido' })
    return
  }

  const phraseRes = await phraseHandler(
    req.method.toUpperCase() === 'POST' ? req.body : req.query,
  )
  const resBody = await phraseRes.json().catch(() => {})

  Array.from(phraseRes.headers.entries()).forEach((value) => {
    res.setHeader(value[0], value[1])
  })
  res.status(phraseRes.status).json(resBody)
}

i18n adaptadores

La cordura no tiene un enfoque prescriptivo para la internacionalización, y hay muchas maneras de implementarla. Este plugin utiliza un patrón de adaptador para permitir la configuración basada en cómo se estructura el contenido y cómo debe ser traducido.

Actualmente, el único adaptador disponible es documentInternationalizationAdapter, que es el que utiliza el plugin oficial de Sanity document-internationalization plugin (version ^2.0.0). Presenta un problema si se requiere un adaptador específico o consulta este repositorio's package/src/adapters/document-internationalization.ts para un ejemplo de cómo implementar uno personalizado.

Transformadores de datos personalizados

Si se requiere transformar datos antes de enviarlos a Phrase, usa la dataTransformers opción. Esto es útil si se cambia la estructura de los datos, o si se excluyen ciertos campos de ser traducidos.

Cada transformador de datos necesita codificar los datos antes de enviarlos a Phrase; y decodificarlos al recibirlos de vuelta para transformarlos antes de guardarlos en Sanity. Se pueden apilar múltiples transformadores y ejecutarlos secuencialmente.

El plugin no ofrece forma de probar transformadores de manera aislada, por lo que el desarrollo puede ser complejo. Guarda documentos de destino reales del conjunto de datos de Sanity en .JSON y úsalos como datos de prueba para cada función de codificación/decodificación.

Ejemplo de modificar archivos VTT codificados en JSON a HTML para que Phrase pueda segmentar mejor el contenido de los subtítulos:

importar { DataTransformer } de 'sanity-plugin-phrase'

const vttJsonTransformer: DataTransformer = {
  codificar: {
    array(arr) {
      // Verifica si el array contiene nodos de subtítulos VTT
      if (
        arr.every(
          (item) =>
            typeof item === 'object' &&
            !!item &&
            '_type' en item &&
            typeof item._type === 'string' &&
            item._type.startsWith('vtt.'),
        )
      ) {
        return encodeSubtitles(arr as StoredSubtitleNode[])
      }
      return undefined // Retornar indefinido para omitir transformación
    },
  },
  decode: {
    object(obj) {
      if (!!obj && '_type' in obj && obj._type === 'encodedSubtitles') {
        return decodeSubtitles(obj as EncodedSubtitles)
      }
      return undefined
    },
  },
}

export const PHRASE_CONFIG = definePhraseOptions({
  // ...
  dataTransformers: [vttJsonTransformer],
})export const PHRASE_CONFIG = definePhraseOptions({
  // ...
  dataTransformers: [vttJsonTransformer],
})

const vttJsonTransformer: DataTransformer = {
  codificar: {
    array(arr) {
      if (
        arr.every(
          (item) =>
            typeof item === 'object' &&
            !!item &&
            '_type' en item &&
            typeof item._type === 'string' &&
            item._type.startsWith('vtt.'),
        )
      ) {
        return encodeSubtitles(arr as StoredSubtitleNode[])
      }

      return undefined
    },
  },
  decode: {
    object(obj) {
      if (!!obj && '_type' in obj && obj._type === 'encodedSubtitles') {
        return decodeSubtitles(obj as EncodedSubtitles)
      }

      return undefined
    },
  },
}

// Omitiendo implementación
// Consulta /demo-nextjs/src/utils/vttJsonTransformer.ts para el código fuente completo
declare function decodeSubtitles(
  encoded: EncodedSubtitles,
): StoredSubtitleNode[]
declare function encodeSubtitles(nodes: StoredSubtitleNode[]): Subtítulos codificados

Limitando el acceso del editor

Los editores que pueden acceder al tablero de Phrase se pueden limitar implementando la opción isPhraseDashboardHidden. Esta función es equivalente a la que se pasa a la propiedad hidden de un campo en Sanity. Recibe un contexto con el usuario y el documento actuales y debe devolver un booleano.

Ejemplo de limitación de acceso al tablero de Phrase para usuarios con el rol de admin:

const PHRASE_CONFIG = definePhraseOptions({
  // ...
  isPhraseDashboardHidden: (context) => {
    const isAdmin = (context.currentUser.roles || []).some(
      (r) => r.name === 'admin',
    )
    // Ocultar si no es un admin
    return !esAdmin
  },
})
¿Fue útil este artículo?

Sorry about that! In what way was it not helpful?

The article didn’t address my problem.
I couldn’t understand the article.
The feature doesn’t do what I need.
Other reason.

Note that feedback is provided anonymously so we aren't able to reply to questions.
If you'd like to ask a question, submit a request to our Support team.
Thank you for your feedback.