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 de Sanity Studio.

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.

  • Retraducciones 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 solicitar traducciones, los editores pueden elegir traducir también los documentos referenciados por el actual y el plugin los vinculará automáticamente por idioma meta.

  • Esquemas flexibles

    Sin importar 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 siguen siendo los mismos; no se requiere volver a capacitar o reconfigurar las 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, use una de las plantillas Starter proporcionadas por Sanity.

Instalación

Navegue al proyecto que contiene la instancia de Sanity Studio e instale el plugin:

npm install sanity-plugin-phrase

# o pnpm, yarn, bun

Variables de entorno

Antes de realizar la configuración del plugin, se deben establecer las siguientes variables de entorno. Crear un archivo `.env` (o `.env.local` para Siguiente.js) en la raíz del proyecto.

Los ejemplos a continuación se proporcionan para SiguienteJS. Para otros marcos de trabajo, consulte su documentación específica y tenga en cuenta que es posible que deba eliminar el prefijo `NEXT_PUBLIC_` para las variables públicas.

Importante

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

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

# URL donde se ubicará el controlador 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"

# API token de Sanity con permisos de escritura (solo para el 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 añade a sanity.config.ts con las opciones de configuración requeridas:

// sanity.config.ts
importar { defineConfig } from 'sanity'
importar {
  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 documento que se pueden traducir
  translatableTypes: ['page', 'post', 'article'],

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

  // Requerido: Idiomas meta a los que los usuarios pueden traducir
  // Usar los mismos códigos que sus documentos de Sanity
  // Esta lista debe tener concordancia con los idiomas definidos en su plantilla de proyecto de Phrase
  supportedTargetLangs: ['es', 'fr', 'de', 'pt'],

  // Requerido: Su URL de endpoint 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 as 'eu' | 'us',

  // Requerido: Plantillas de proyecto de Phrase disponibles para los editores
  phraseTemplates: [
    {
      templateUid: 'YOUR_TEMPLATE_UID_HERE',
      label: '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}`
  },

  // Mostrar configuración opcional

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

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

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

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

  // Ocultar tablero Phrase basado en roles de usuario
  isPhraseDashboardHidden: (context) =>
    !(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})

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

const PHRASE_CONFIG = definePhraseOptions({
  /**
   * El adaptador i18n a usar para este plugin.
   * Será responsable de obtener y modificar documentos para cada idioma meta.
   *
   * Vea a continuación más información sobre los 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 usuario pueden traducir.
   * Debe ser el mismo que el almacenado en sus documentos de Sanity y utilizado por su front-end. El plugin traducirá automáticamente al formato de Phrase.
   */
  supportedTargetLangs: ['cz', 'es', 'pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru'],

  /**
   * código de idioma del idioma fuente que se traducirá.
   * Debe ser el mismo que el almacenado en sus documentos de Sanity y utilizado por su front-end. El plugin 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 del plugin configurada.
   *
   * **Nota:** siga los pasos para configurar el endpoint, descritos a continuación
   */
  apiEndpoint: 'https://my-site.com/api/phrase',

  /**
   * Se utiliza para redirigir a los lingüistas desde el tablero de Phrase a la vista previa de sus traducciones en el front-end.
   */
  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 configurar las plantillas, descritos a continuación
   */
  phraseTemplates: [
    {
      templateUid: '1jYg0Pc1d8kAHUyM0tgdmt',
      label: '[Sanity.io] Plantilla predeterminada',
    },
  ],

  /**
   * @Opcional
   * En caso de que desee mostrar u ocultar el tablero de Phrase según los privilegios de usuario.
   *
   * Recibe un contexto con el usuario y el documento actuales y debe devolver un valor 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 que el plugin sepa qué tipo de documento se pueden traducir, pase una matriz de tipo de documento a la función injectPhraseIntoSchema en el archivo sanity.config.ts:

// sanity.config.ts
import { injectPhraseIntoSchema } from 'sanity-plugin-phrase'

// lista de tipo de esquema traducible. Generalmente exportado desde un archivo de índice
// donde sea que tenga ubicado su esquema de Sanity
const TRANSLATABLE_SCHEMAS = ['page', 'post', 'course', 'lesson', 'definition']

export default defineConfig({
  schema: {
    types: injectPhraseIntoSchema(TRANSLATABLE_SCHEMAS, PHRASE_CONFIG),
    templates: (prev) =>
      prev.filtrar((plantilla) => !TRANSLATABLE_SCHEMAS.includes(plantilla.ID)),
  },
  plugins: [
    // ...
    phrasePlugin({
      // Sus opciones de configuración aquí
    }),
  ],
})

Exclusión de PTD de las lista de documento

Los PTD (Phrase Translation Documents) son documento temporales que no deberían aparecer en las lista de documento normales dentro de Sanity Studio. La constante NOT_PTD proporciona un filtro GROQ para este propósito:

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

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

Ocultar el menú de traducción de los PTDs

Al usar el plugin document-internationalization, el menú de traducción debe ocultarse de los PTDs. La utilidad isPtdId identifica documentos PTD:

import { isPtdId } from 'sanity-plugin-phrase/utils'
import { DocumentInternationalizationMenu } from '@sanity/document-internationalization'

// usar el mismo array que translatableTypes de su PHRASE_CONFIG
const TRANSLATABLE_TYPES = ['page', 'post', 'article']

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

      // Solo mostrar el menú de traducción para documentos reales, no para 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 usuario de Phrase, pero Phrase debe configurarse para enviar notificaciones de webhook al endpoint de la API del backend. Esto permite actualizaciones en tiempo real a medida que avanza el progreso de las traducciones.

Crear un webhook

Crear un webhook con esta configuración:

  • URL

    La URL al endpoint de la API del plugin, tal como se configuró 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 pueda mantener los datos de Sanity sincronizados.

Configurar plantilla(s) de proyecto

Configure la(s) plantilla(s) de proyecto de Phrase con las propiedades requeridas para los flujos de trabajo y los requisitos del Team. Se puede ofrecer una o más plantillas para elegir al solicitar una nueva traducción. Las plantillas de proyecto de Phrase deben tener una configuración de importación JSON específica para que el plugin funcione correctamente. Esta configuración controla qué campos se envían a los traductores y cuáles se conservan como metadatos.

Importar archivo JSON

Excluir claves específicas (usar regexp):

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

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

  • Excluir datos específicos de localización, como el idioma de un documento determinado si se utiliza @sanity/document-internationalization.

  • Incluir cualquier clave específica del proyecto que no requiera traducción, como un slug para contenido que usa la misma ruta en todos los idiomas. Reemplazar YOUR_IGNORED_KEYS_HERE 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 el sourceLanguage del plugin.

Idiomas meta

Asegurarse 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 usa para comunicarse con Sanity Studio. Se utiliza para autenticarse en la API de Phrase, recibir webhooks y solicitudes de usuario desde Sanity Studio.

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

Acceder configurar el manejador con un patrón de Solicitud-Respuesta mediante importar {createRequestHandler} desde sanity-plugin-phrase/backend o usar el manejador interno directamente mediante importar {createInternalHandler} desde sanity-plugin-phrase/backend. Asegurarse de que las solicitudes CORS se manejen correctamente, ya que el estudio y el endpoint tienen orígenes diferentes.

El directorio app de NextJS no es compatible actualmente, ya que analiza incorrectamente el manejador de backend como un componente de cliente de React.

Este ejemplo demuestra cómo crear un Route Handler en la ruta apiEndpoint configurada en /api/phrase usando el Pages Router de Next.js:

// app/api/phrase/route.ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
importar type { NextApiRequest, NextApiResponse } from 'next'
importar { Phrase_CONFIG } from 'phraseConfig'
importar { createInternalHandler } from 'sanity-plugin-Phrase/backend'

importar { writeToken } from '~/lib/sanity.API'
importar { cliente } from '~/lib/sanity.cliente'

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

const phraseHandler = createInternalHandler({
  phraseCredentials: {
    userName: process.env.Phrase_usuario_NAME || '',
    password: process.env.Phrase_PASSWORD || '',
  },
  sanityClient: cliente.withConfig({ identificador único (token): writeToken }),
  pluginOptions: Phrase_CONFIG,
})

exportar default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  res.setHeader('acceder-Control-Allow-Origin', '*')
  res.setHeader('acceder-Control-Allow-Methods', 'POST, OPTIONS')
  res.setHeader('acceder-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: 'Method not allowed' })
    return
  }

  const phraseRes = await phraseHandler(
    req.method.toUpperCase() === 'POST' ? req.body : req.consulta,
  )
  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
// Next.js API route: https://nextjs.org/docs/pages/building-your-application/routing/api-routes
importar type { NextApiRequest, NextApiResponse } from 'next'
importar { Phrase_CONFIG } from 'phraseConfig'
importar { createInternalHandler } from 'sanity-plugin-Phrase/backend'
importar { writeToken } from '~/lib/sanity.API'
importar { cliente } from '~/lib/sanity.cliente'

const phraseHandler = createInternalHandler({
  phraseCredentials: {
    userName: process.env.Phrase_usuario_NAME || '',
    password: process.env.Phrase_PASSWORD || '',
  },
  sanityClient: cliente.withConfig({ identificador único (token): writeToken }),
  pluginOptions: Phrase_CONFIG,
})

exportar default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  res.setHeader('acceder-Control-Allow-Origin', '*')
  res.setHeader('acceder-Control-Allow-Methods', 'POST, OPTIONS')
  res.setHeader('acceder-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: 'Method not allowed' })
    return
  }

  const phraseRes = await phraseHandler(
    req.method.toUpperCase() === 'POST' ? req.body : req.consulta,
  )
  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)
}

adaptadores i18n

Sanity no tiene un enfoque prescriptivo para la internacionalización, y existen muchas formas de implementarla. Este plugin usar 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 utilizado por el plugin document-internationalization oficial de Sanity (versión ^2.0.0). Presente un problema si se requiere un adaptador específico o consulte este repositorio package/src/adapters/document-internationalization.ts para obtener un ejemplo sobre cómo implementar uno personalizar.

Transformadores de datos personalizar

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

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 ninguna forma de probar transformadores de forma aislada, por lo que el desarrollo puede ser complejo. Guardar documentos meta reales del conjunto de datos de Sanity en .JSON y usarlos como datos de prueba para cada función de codificación/decodificación.

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

import { DataTransformer } from 'sanity-plugin-phrase'

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

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

const vttJsonTransformer: DataTransformer = {
  encode: {
    array(arr) {
      if (
        arr.every(
          (item) =>
            typeof item === 'object' &&
            !!item &&
            '_type' in 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
// Consulte /demo-nextjs/src/utils/vttJsonTransformer.ts para obtener el código fuente completo
declare function decodeSubtitles(
  encoded: EncodedSubtitles,
): StoredSubtitleNode[]
declare function encodeSubtitles(nodes: StoredSubtitleNode[]): EncodedSubtitles

Limitar el acceso del editor

Se puede limitar qué editores pueden acceder al tablero de Phrase 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 cómo limitar el acceso al tablero de Phrase a 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 admin
    return !isAdmin
  },
})
¿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.