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.
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),
],
})
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
},
},
})
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 .
-
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_HEREcon 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.
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 o usar el manejador interno directamente mediante importar {createInternalHandler} desde . 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 , 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 . 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 i. 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
},
})