Integrazioni

Sanity (TMS)

Contenuti tradotti automaticamente dall'inglese con Phrase Language AI.

Il plugin fornisce accesso ai contenuti tradotti di Phrase all'interno dello studio Sanity.

Sono supportate solo le traduzioni a livello di documento. Le traduzioni a livello di campo non sono supportate.

Caratteristiche:

  • Anteprime in tempo reale

    Le traduzioni sono mantenute sincronizzate per consentire a linguisti e traduttori di vedere le modifiche in anteprima in tempo reale.

  • Ritraduzioni intelligenti

    Il plugin confronta quali contenuti sono cambiati dall'ultima traduzione e invia solo quelle modifiche a Phrase.

  • Traduzione automatica dei riferimenti

    Quando si emettono traduzioni, gli editor possono scegliere di tradurre anche i documenti a cui si fa riferimento e il plugin li collegherà automaticamente per lingua di destinazione.

  • Schemi flessibili

    Non importa la struttura, il plugin si adatta ad essa e garantisce che il contenuto tradotto finale sia conforme agli schemi di Sanity.

  • Flussi di lavoro di Phrase

    I flussi di lavoro di traduzione in Phrase rimangono gli stessi; non è necessario riaddestrare o riconfigurare le operazioni.

Installazione del Plugin di Sanity

L'installazione viene eseguita dalla riga di comando.

Si presume che un generatore di siti web e Sanity Studio siano già configurati. Se non lo sono, utilizza uno dei modelli starter forniti da Sanity.

Installazione

Naviga al progetto contenente l'istanza di Sanity Studio e installa il plugin:

npm install sanity-plugin-phrase

# o pnpm, yarn, bun

Variabili di ambiente

Prima di configurare il plugin, le seguenti variabili di ambiente devono essere configurate. Crea un file `.env` (o `.env.local` per Next.js) nella radice del progetto.

Gli esempi qui sotto sono forniti per NextJS. Per altri framework, fare riferimento alla loro documentazione specifica e notare che il prefisso `NEXT_PUBLIC_` potrebbe dover essere rimosso per le variabili pubbliche.

Importante

Le variabili lato server (SANITY_WRITE_TOKEN, PHRASE_USER_NAME, PHRASE_PASSWORD) non dovrebbero mai essere esposte al client. In Next.js, solo le variabili con prefisso NEXT_PUBLIC_ sono esposte al browser.

# URL di base del tuo sito (utilizzato per i link di anteprima)
NEXT_PUBLIC_BASE_URL="http://localhost:3000"

# URL dove si troverà il gestore backend del plugin
NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT="http://localhost:3000/api/phrase"

# Regione del datacenter di Phrase ('eu' o 'us')
NEXT_PUBLIC_PHRASE_REGION="eu"

# Configurazione del progetto Sanity
NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
NEXT_PUBLIC_SANITY_DATASET="production"

# Token API di Sanity con permessi di scrittura (solo lato server)
SANITY_WRITE_TOKEN=""

# Credenziali di Phrase (solo lato server)
# Nota: L'API di Phrase si aspetta solo la parte del nome utente, NON l'indirizzo email completo
PHRASE_USER_NAME="phraseUsername"
PHRASE_PASSWORD="secretPassword"

Configurazione del plugin

Il plugin è aggiunto a sanity.config.ts con le opzioni di configurazione richieste:

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

const PHRASE_CONFIG = definePhraseOptions({
  // Obbligatorio: adattatore i18n per l'internazionalizzazione del documento
  i18nAdapter: documentInternationalizationAdapter(),

  // Obbligatorio: Tipi di documento che possono essere tradotti
  tipiTraducibili: ['pagina', 'post', 'articolo'],

  // Obbligatorio: Lingua sorgente (lingua principale)
  // Questo deve corrispondere alla lingua definita nel modello del progetto Phrase
  linguaSorgente: 'en',

  // Obbligatorio: Lingue di destinazione a cui gli utenti possono tradurre
  // Usa gli stessi codici dei tuoi documenti Sanity
  // Questo elenco deve corrispondere alle lingue definite nel modello del progetto Phrase
  lingueDestinazioneSupportate: ['es', 'fr', 'de', 'pt'],

  // Obbligatorio: L'URL del tuo endpoint API backend
  apiEndpoint: process.env.NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT!,

  // Obbligatorio: Regione del data center Phrase ('eu' o 'us')
  phraseRegion: process.env.NEXT_PUBLIC_PHRASE_REGION come 'eu' | 'us',

  // Obbligatorio: Frasi modelli di progetto disponibili per gli editor
  fraseModelli: [
    {
      templateUid: 'IL_TUO_TEMPLATE_UID_QUI',
      etichetta: 'Modello di Traduzione Predefinito',
    },
  ],

  // Obbligatorio: Genera URL di anteprima per i linguisti
  getDocumentPreview: (doc, sanityClient) => {
    const publishedId = doc._id.replace('drafts.', '')
    return `${process.env.NEXT_PUBLIC_BASE_URL}/api/draft?id=${publishedId}`
  },

  // Impostazioni opzionali

  // Profondità massima per tradurre documenti referenziati (predefinito: 3)
  maxReferencesDepth: 3,

  // Consenti la traduzione di documenti bozza (predefinito: false)
  translateDrafts: false,

  // Trasformatori di dati personalizzati per tipi di contenuto speciali
  dataTransformers: [],

  // Configurazione di logging per il debug
  logger: {
    minimumLogLevel: 'info', // 'debug' | 'info' | 'warning' | 'error' | 'fatal'
  },

  // Nascondi Phrase dashboard in base ai ruoli utente
  isPhraseDashboardHidden: (context) =>
    !(context.currentUser.roles || []).some((r) => r.name === 'amministratore'),
})

esporta default defineConfig({
  // ... la tua configurazione esistente
  plugin: [
    phrasePlugin(PHRASE_CONFIG),
    // ... altri plugin
  ],
})// sanity.config.(js|ts)
importa {
  phrasePlugin,
  documentInternationalizationAdapter,
} da 'sanity-plugin-phrase'

const PHRASE_CONFIG = definePhraseOptions({
  /**
   * L'adattatore i18n da utilizzare per questo plugin.
   * Sarà responsabile del recupero e della modifica dei documenti per ogni lingua di destinazione.
   *
   * Vedi sotto per ulteriori informazioni sugli adattatori.
   */
  i18nAdapter: documentInternationalizationAdapter(),

  /**
   * Tipi di schema di Sanity che il plugin può tradurre
   */
  tipiTraslazionali: ['pagina', 'post', 'corso', 'lezione', 'definizione'],

  /**
   * Codice della lingua di tutte le lingue a cui gli utenti possono tradurre.
   * Dovrebbe essere lo stesso di quello memorizzato nei tuoi documenti Sanity e utilizzato dal tuo front-end. Il plugin lo tradurrà automaticamente nel formato di Phrase.
   */
  lingueDestinazioneSupportate: ['cz', 'es', 'pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru'],

  /**
   * Codice della lingua sorgente che sarà tradotto.
   * Dovrebbe essere lo stesso di quello memorizzato nei tuoi documenti Sanity e utilizzato dal tuo front-end. Il plugin lo tradurrà automaticamente nel formato di Phrase.
   */
  linguaSorgente: 'en',

  /**
   * Come definito nelle impostazioni del tuo account Phrase
   * O `eu` o `us`
   */
  phraseRegion: 'us|eu',

  /**
   * L'URL per il tuo backend API del plugin configurato.
   *
   * **Nota:** segui i passaggi per impostare l'endpoint, descritti di seguito
   */
  apiEndpoint: 'https://my-site.com/api/phrase',

  /**
   * Usato per reindirizzare i linguisti dal dashboard di Phrase all'anteprima front-end delle loro traduzioni.
   */
  getDocumentPreview: async (doc, sanityClient) => {
    const publishedId = doc._id.replace('drafts.', '')
    return `${process.env.NEXT_PUBLIC_FRONT_END_URL}/api/draft?publishedId=${publishedId}`
  },

  /**
   * Modelli di progetto Phrase che i tuoi editor possono utilizzare quando richiedono traduzioni.
   *
   * **Nota:** segui i passaggi per impostare i modelli, descritti di seguito
   */
  fraseModelli: [
    {
      templateUid: '1jYg0Pc1d8kAHUyM0tgdmt',
      etichetta: '[Sanity.io] Modello predefinito',
    },
  ],

  /**
   * @opzionale
   * Nel caso tu voglia mostrare o nascondere il dashboard di Phrase in base ai privilegi dell'utente.
   *
   * Riceve un contesto con l'utente e il documento attuali e deve restituire un booleano.
   */
  isPhraseDashboardHidden: (context) =>
    !(context.currentUser.roles || []).some((r) => r.name === 'amministratore'),
})

esporta default defineConfig({
  // ...
  plugin: [
    // ...
    phrasePlugin(PHRASE_CONFIG),
  ],
})

Configurazione del Plugin Sanity

Iniezione dello schema

Per dire al plugin quali tipi di documento possono essere tradotti, passa un array di tipi di documento alla funzione injectPhraseIntoSchema nel file sanity.config.ts:

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

// Elenco dei tipi di schema traducibili. Di solito esportato da un file indice
// ovunque tu abbia il tuo schema di Sanity
const TRANSLATABLE_SCHEMAS = ['page', 'post', 'course', 'lesson', 'definition']

esporta default defineConfig({
  schema: {
    types: injectPhraseIntoSchema(TRANSLATABLE_SCHEMAS, PHRASE_CONFIG),
    modelli: (prev) =>
      prev.filter((template) => !TRANSLATABLE_SCHEMAS.includes(template.id)),
  },
  plugin: [
    // ...
    phrasePlugin({
      // Le tue opzioni di configurazione qui
    }),
  ],
})

Escludendo i PTD dalle liste di documenti

I PTD (Documenti di Traduzione di Frasi) sono documenti temporanei che non dovrebbero apparire nelle normali liste di documenti all'interno di Sanity Studio. La costante NON_PTD fornisce un filtro GROQ per questo scopo:

// sanity.config.ts
importa { NON_PTD } da 'sanity-plugin-phrase/utils'

esporta default defineConfig({
  // ... altra configurazione
  plugin: [
    structureTool({
      struttura: (S) =>
        S.list()
          .titolo('Contenuto')
          .items([
            S.listItem()
              .title('Posts')
              .schemaType('post')
              .child(
                S.documentList()
                  .title('Posts')
                  .filter(`_type == "post" && ${NOT_PTD}`),
              ),
            // ... altri elementi
          ]),
    }),
  ],
})

Nascondere il menu di traduzione dai PTD

Quando si utilizza il plugin di internazionalizzazione del documento, il menu di traduzione dovrebbe essere nascosto dai PTD. L'utilità isPtdId identifica i documenti PTD:

importare { isPtdId } da 'sanity-plugin-phrase/utils'
importare { DocumentInternationalizationMenu } da '@sanity/document-internationalization'

// Usa lo stesso array di translatableTypes dalla tua PHRASE_CONFIG
const TRANSLATABLE_TYPES = ['page', 'post', 'article']

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

      // Mostra solo il menu di traduzione per documenti reali, non PTD
      return TRANSLATABLE_TYPES.includes(schemaType) &&
        documentId &&
        !isPtdId(documentId)
        ? [...prev, DocumentInternationalizationMenu]
        : prev
    },
  },
})

Configurazione della Phrase

Il plugin non ha configurazione nell'interfaccia di Phrase, ma Phrase deve essere configurato per inviare notifiche webhook all'endpoint API del backend. Questo consente aggiornamenti in tempo reale man mano che le traduzioni progrediscono.

Crea un webhook

Crea un webhook con queste impostazioni:

  • URL

    L'URL dell'endpoint API del plugin, come configurato nell'opzione apiEndpoint.

  • Eventi:

    • Lavori

      • Lavoro eliminato

      • Job assigned

      • Data di scadenza lavoro modificata

      • Destinazione lavoro aggiornato

    • Progetti

      • Progetto eliminato

      • Data di scadenza progetto modificata

    • Altro

      • Pre-traduzione terminata

Questo assicura che il plugin venga notificato di eventuali modifiche ai progetti Phrase e possa mantenere i dati di Sanity sincronizzati.

Configurazione del modello(i) di progetto

Configura il modello di progetto di Phrase con le proprietà richieste per i flussi di lavoro e le esigenze del team. Uno o più modelli possono essere offerti per la scelta quando si ordina una nuova traduzione. I modelli di progetto di Phrase devono avere impostazioni di importazione JSON specifiche affinché il plugin funzioni correttamente. Queste impostazioni controllano quali campi vengono inviati ai traduttori e quali vengono preservati come metadati.

Importazione file JSON

Usa regex per escludere chiavi specifiche:

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

Questa espressione include chiavi duplicate di proposito per assicurarsi che vengano ignorate dal parser RegEx di Phrase. Assicurati che siano correttamente duplicate.

  • Escludi dati specifici della localizzazione, come la lingua di un dato documento se si utilizza @sanity/document-internationalization.

  • Includi eventuali chiavi specifiche del progetto che non richiedono traduzione, come uno slug per contenuti che utilizzano lo stesso percorso in tutte le lingue. Sostituisci LE_TUE_CHIAVI_IGNORATE_QUI con un elenco di chiavi separate da pipe da ignorare.

  • Nota di contesto: /_sanityContext

Lingua di origine

Attualmente, questo plugin opera assumendo di avere una singola lingua di origine. Il/i modello/i del progetto devono avere la stessa origine di quella configurata nel sourceLanguage del plugin.

Lingue di destinazione

Assicurati che le lingue scelte in Phrase siano in sincronizzazione con quelle nella configurazione del plugin.

Sanity apiEndpoint

Questo è l'endpoint che il plugin utilizza per comunicare con il Sanity Studio. Viene utilizzato per autenticarsi all'API di Phrase, ricevere webhook e richieste utente dallo studio Sanity.

Crea un endpoint API personalizzato nel progetto Sanity per gestire queste richieste. Uno dei modi più semplici per farlo è utilizzare funzioni serverless tramite framework front-end come NextJS, Remix, SvelteKit o Nuxt.

Accedi a configurare il gestore con un modello di Richiesta-Risposta tramite import {createRequestHandler} da sanity-plugin-phrase/backend o utilizza direttamente il gestore interno tramite import {createInternalHandler} da sanity-plugin-phrase/backend. Assicurati che le richieste CORS siano gestite correttamente se lo studio e l'endpoint hanno origini diverse.

La directory app di NextJS non è attualmente supportata poiché analizza in modo errato il gestore backend come un componente client React.

Questo esempio dimostra come creare un Gestore di Route nel percorso configurato apiEndpoint a /api/phrase utilizzando il Router delle Pagine di Next.js:

// app/api/phrase/route.ts
// Supporto per le route API di Next.js: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
import { PHRASE_CONFIG } from 'phraseConfig'
import { createInternalHandler } from 'sanity-plugin-phrase/backend'

import { writeToken } from '~/lib/sanity.api'
importa { cliente } da '~/lib/sanity.client'

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

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

esporta 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: 'Metodo non consentito' })
    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
// Rotta API di Next.js: https://nextjs.org/docs/pages/building-your-application/routing/api-routes
import type { NextApiRequest, NextApiResponse } from 'next'
import { PHRASE_CONFIG } from 'phraseConfig'
import { createInternalHandler } from 'sanity-plugin-phrase/backend'
import { writeToken } from '~/lib/sanity.api'
importa { cliente } da '~/lib/sanity.client'

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

esporta 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: 'Metodo non consentito' })
    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)
}

adattatori i18n

Sanity non ha un approccio prescrittivo all'internazionalizzazione e ci sono molti modi per implementarlo. Questo plugin utilizza un modello di adattatore per consentire la configurazione in base a come è strutturato il contenuto e come dovrebbe essere tradotto.

Attualmente, l'unico adattatore disponibile è documentInternationalizationAdapter, che è quello utilizzato dal plugin ufficiale di Sanity document-internationalization (versione ^2.0.0). Segnala un problema se è richiesto un adattatore specifico o fai riferimento a questo repository's package/src/adapters/document-internationalization.ts per un esempio su come implementarne uno personalizzato.

Trasformatori di dati personalizzati

Se è necessario trasformare i dati prima di inviarli a Phrase, utilizzare l'opzione dataTransformers. Questo è utile se si cambia la struttura dei dati o se si escludono determinati campi dalla traduzione.

Ogni trasformatore di dati deve codificare i dati prima di inviarli a Phrase; e decodificarli quando li riceve indietro per trasformarli prima di salvarli in Sanity. Più trasformatori possono essere impilati e eseguiti in sequenza.

Il plugin non offre alcun modo per testare i trasformatori in isolamento, quindi lo sviluppo può essere complesso. Salva documenti target reali dal dataset di Sanity in .JSON e usali come dati di test per ciascuna funzione di codifica/decodifica.

Esempio di modifica di file VTT codificati in JSON in HTML affinché Phrase possa meglio segmentare il contenuto dei sottotitoli:

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

const vttJsonTransformer: DataTransformer = {
  encode: {
    array(arr) {
      // Controlla se l'array contiene nodi di sottotitoli 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 // Restituisci undefined per saltare la trasformazione
    },
  },
  decodifica: {
    object(obj) {
      se (!!obj && '_type' in obj && obj._type === 'encodedSubtitles') {
        ritorna decodeSubtitles(obj come EncodedSubtitles)
      }
      ritorna indefinito
    },
  },
}

esporta const PHRASE_CONFIG = definePhraseOptions({
  // ...
  dataTransformers: [vttJsonTransformer],
})esporta 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[])
      }

      ritorna indefinito
    },
  },
  decodifica: {
    object(obj) {
      se (!!obj && '_type' in obj && obj._type === 'encodedSubtitles') {
        ritorna decodeSubtitles(obj come EncodedSubtitles)
      }

      ritorna indefinito
    },
  },
}

// Implementazione saltata
// Fare riferimento a /demo-nextjs/src/utils/vttJsonTransformer.ts per il codice sorgente completo
dichiara funzione decodeSubtitles(
  codificato: EncodedSubtitles,
): StoredSubtitleNode[]
dichiara funzione encodeSubtitles(nodi: StoredSubtitleNode[]): EncodedSubtitles

Limitare l'accesso dell'editor

Quali editor possono accedere al dashboard di Phrase può essere limitato implementando l'opzione isPhraseDashboardHidden. Questa funzione è equivalente a quella passata alla proprietà nascosta di un campo in Sanity. Riceve un contesto con l'utente e il documento attuali e deve restituire un booleano.

Esempio di limitazione dell'accesso al dashboard di Phrase agli utenti con il ruolo di admin:

const PHRASE_CONFIG = definePhraseOptions({
  // ...
  isPhraseDashboardHidden: (context) => {
    const isAdmin = (contesto.currentUser.ruoli || []).some(
      (r) => r.nome === 'amministratore',
    )
    // Nascondi se non sei un amministratore
    return !èAmministratore
  },
})
Questo articolo ti è stato utile?

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.