Intégrations

Sanity (TMS)

Le contenu est traduit de l’anglais par Phrase Language AI.

Le plug-in fournit un accès au contenu traduit de Phrase depuis le studio Sanity.

Seules les traductions au niveau du document sont prises en charge. Les traductions au niveau des champs ne sont pas prises en charge.

Fonctionnalités :

  • Aperçus en temps réel

    Les traductions sont synchronisées pour permettre aux linguistes et aux traducteurs de voir les changements d'aperçu en temps réel.

  • Re-traductions intelligentes

    Le plug-in compare le contenu qui a changé depuis la dernière traduction et n'envoie que ces changements à Phrase.

  • Traduction automatique des références

    Lors de l'émission des traductions, les éditeurs peuvent choisir de traduire également les documents référencés par le courant et le plug-in les liera automatiquement par langue cible.

  • Schémas flexibles

    Peu importe la structure, le plug-in s'adapte à celle-ci et garantit que le contenu traduit final est conforme aux schémas de Sanity.

  • Flux de travail Phrase

    Les flux de travail de traduction dans Phrase restent les mêmes ; il n'est pas nécessaire de réentraîner ou de reconfigurer les opérations.

Installation du plug-in Sanity

L'installation se fait en ligne de commande.

On suppose qu'un générateur de site web et Sanity Studio sont déjà configurés. Si ce n'est pas le cas, utilisez l'un des modèles de démarrage fournis par Sanity.

Installation

Accédez au projet contenant l'instance de Sanity Studio et installez le plug-in :

npm install sanity-plugin-phrase

# ou pnpm, yarn, bun

Variables d'environnement

Avant de configurer le plug-in, les variables d'environnement suivantes doivent être configurées. Créez un fichier `.env` (ou `.env.local` pour Next.js) à la racine du projet.

Les exemples ci-dessous sont donnés pour NextJS. Pour d'autres frameworks, référez-vous à leur documentation spécifique, et notez que le préfixe `NEXT_PUBLIC_` peut devoir être supprimé pour les variables publiques.

Important

Les variables côté serveur (SANITY_WRITE_TOKEN, PHRASE_USER_NAME, PHRASE_PASSWORD) ne doivent jamais être exposées au client. Dans Next.js, seules les variables préfixées par NEXT_PUBLIC_ sont exposées au navigateur.

# URL de base de votre site (utilisée pour les liens d'aperçu)
NEXT_PUBLIC_BASE_URL="http://localhost:3000"

# URL où le gestionnaire de backend du plug-in sera situé
NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT="http://localhost:3000/api/phrase"

# Région du datacenter Phrase ('eu' ou 'us')
NEXT_PUBLIC_PHRASE_REGION="eu"

# Configuration du projet Sanity
NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
NEXT_PUBLIC_SANITY_DATASET="production"

# Jeton API Sanity avec permissions d'écriture (côté serveur uniquement)
SANITY_WRITE_TOKEN=""

# Informations d'identification Phrase (côté serveur uniquement)
# Remarque : L'API Phrase attend uniquement la partie nom d'utilisateur, PAS l'adresse e-mail complète
PHRASE_USER_NAME="phraseUsername"
PHRASE_PASSWORD="secretPassword"

Configuration du plugin

Le plugin est ajouté à sanity.config.ts avec les options de configuration requises :

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

const PHRASE_CONFIG = definePhraseOptions({
  // Requis : adaptateur i18n pour l'internationalisation des documents
  i18nAdapter: documentInternationalizationAdapter(),

  // Requis : Types de documents pouvant être traduits
  translatableTypes: ['page', 'post', 'article'],

  // Requis : Langue source (langue principale)
  // Cela doit correspondre à la langue définie dans votre modèle de projet Phrase
  sourceLang: 'en',

  // Requis : Langues cibles vers lesquelles les utilisateurs peuvent traduire
  // Utilisez les mêmes codes que vos documents Sanity
  // Cette liste doit correspondre aux langues définies dans votre modèle de projet Phrase
  supportedTargetLangs: ['es', 'fr', 'de', 'pt'],

  // Requis : L'URL de votre point de terminaison API backend
  pointDeFin: process.env.NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT!

  // Requis : Région du centre de données de la phrase ('eu' ou 'us')
  phraseRegion: process.env.NEXT_PUBLIC_PHRASE_REGION comme 'eu' | 'us',

  // Requis : Modèles de projet de phrase disponibles pour les éditeurs
  phraseTemplates: [
    {
      templateUid: 'VOTRE_TEMPLATE_UID_ICI',
      étiquette: 'Modèle de traduction par défaut',
    },
  ],

  // Requis : Générer des URL d'aperçu pour les linguistes
  getDocumentPreview: (doc, sanityClient) => {
    const publishedId = doc._id.replace('drafts.', '')
    return `${process.env.NEXT_PUBLIC_BASE_URL}/api/draft?id=${publishedId}`
  },

  // Paramètres optionnels

  // Profondeur maximale pour traduire les documents référencés (par défaut: 3)
  maxReferencesDepth: 3,

  // Autoriser la traduction des documents brouillons (par défaut: faux)
  translateDrafts: faux,

  // Transformateurs de données personnalisés pour des types de contenu spéciaux
  dataTransformers: [],

  // Configuration de journalisation pour le débogage
  logger: {
    niveauDeJournalMinimum: 'info', // 'debug' | 'info' | 'warning' | 'error' | 'fatal'
  },

  // Masquer le tableau de bord Phrase en fonction des rôles des utilisateurs
  isPhraseDashboardHidden: (contexte) =>
    !(contexte.utilisateurActuel.roles || []).some((r) => r.name === 'administrateur'),
})

exporter par défaut defineConfig({
  // ... votre configuration existante
  plugins: [
    phrasePlugin(PHRASE_CONFIG),
    // ... autres plugins
  ],
})// sanity.config.(js|ts)
import {
  phrasePlugin,
  documentInternationalizationAdapter,
} from 'sanity-plugin-phrase'

const PHRASE_CONFIG = definePhraseOptions({
  /**
   * L'adaptateur i18n à utiliser pour ce plugin.
   * Il sera responsable de la récupération et de la modification des documents pour chaque langue cible.
   *
   * Voir ci-dessous pour plus d'informations sur les adaptateurs.
   */
  i18nAdapter: documentInternationalizationAdapter(),

  /**
   * Types de schéma Sanity que le plugin peut traduire
   */
  typesTraduisibles: ['page', 'post', 'cours', 'leçon', 'définition'],

  /**
   * Code de langue de toutes les langues vers lesquelles les utilisateurs peuvent traduire.
   * Doit être le même que celui stocké dans vos documents Sanity et utilisé par votre front-end. Le plugin le traduira automatiquement au format de Phrase.
   */
  supportedTargetLangs: ['cz', 'es', 'pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru']

  /**
   * Code de langue de la langue source qui sera traduite.
   * Doit être le même que celui stocké dans vos documents Sanity et utilisé par votre front-end. Le plugin le traduira automatiquement au format de Phrase.
   */
  sourceLang: 'en',

  /**
   * Comme défini par les paramètres de votre compte Phrase
   * Soit `eu` soit `us`
   */
  phraseRegion: 'us|eu',

  /**
   * L'URL de votre API backend de plugin configuré.
   *
   * **Remarque :** suivez les étapes pour configurer le point de terminaison, décrites ci-dessous
   */
  apiEndpoint: 'https://my-site.com/api/phrase',

  /**
   * Utilisé pour rediriger les linguistes du tableau de bord Phrase vers l'aperçu frontal de leurs traductions.
   */
  getDocumentPreview: async (doc, sanityClient) => {
    const publishedId = doc._id.replace('drafts.', '')
    return `${process.env.NEXT_PUBLIC_FRONT_END_URL}/api/draft?publishedId=${publishedId}`
  },

  /**
   * Modèles de projet Phrase que vos éditeurs peuvent utiliser lors de la demande de traductions.
   *
   * **Remarque :** suivez les étapes pour définir les modèles, décrites ci-dessous
   */
  phraseTemplates: [
    {
      templateUid: '1jYg0Pc1d8kAHUyM0tgdmt',
      étiquette: '[Sanity.io] Modèle par défaut',
    },
  ],

  /**
   * @optional
   * Au cas où vous voudriez afficher ou masquer le tableau de bord Phrase selon les privilèges de l'utilisateur.
   *
   * Reçoit un contexte avec l'utilisateur actuel et le document et doit retourner un booléen.
   */
  isPhraseDashboardHidden: (contexte) =>
    !(contexte.utilisateurActuel.roles || []).some((r) => r.name === 'administrateur'),
})

exporter par défaut defineConfig({
  // ...
  plugins: [
    // ...
    phrasePlugin(PHRASE_CONFIG),
  ],
})

Configuration du plugin Sanity

Injection de schéma

Pour indiquer au plugin quels types de documents peuvent être traduits, passez un tableau de types de documents à la fonction injectPhraseIntoSchema dans le fichier sanity.config.ts :

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

// Liste des types de schéma traduisibles. Généralement exporté d'un fichier index
// où que vous ayez votre schéma Sanity
const SCHEMAS_TRADUISIBLES = ['page', 'post', 'cours', 'leçon', 'définition']

exporter par défaut defineConfig({
  schéma: {
    types: injectPhraseIntoSchema(SCHEMAS_TRADUISIBLES, CONFIG_PHRASE),
    modèles: (précédent) =>
      précédent.filter((modèle) => !SCHEMAS_TRADUISIBLES.includes(modèle.id)),
  },
  plugins: [
    // ...
    phrasePlugin({
      // Vos options de configuration ici
    }),
  ],
})

Exclure les PTDs des listes de documents

Les PTDs (Documents de Traduction de Phrase) sont des documents temporaires qui ne devraient pas apparaître dans les listes de documents normales à l'intérieur de Sanity Studio. La constante NOT_PTD fournit un filtre GROQ à cet effet :

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

exporter par défaut defineConfig({
  // ... autre config
  plugins: [
    structureTool({
      structure: (S) =>
        S.list()
          .title('Content')
          .items([
            S.listItem()
              .title('Posts')
              .schemaType('post')
              .child(
                S.documentList()
                  .title('Posts')
                  .filtre(`_type == "post" && ${NOT_PTD}`),
              ),
            // ... autres éléments
          ]),
    }),
  ],
})

Masquer le menu de traduction des PTDs

Lors de l'utilisation du plug-in d'internationalisation de document, le menu de traduction doit être masqué pour les PTDs. L'utilitaire isPtdId identifie les documents PTD :

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

// Utilisez le même tableau que translatableTypes de votre PHRASE_CONFIG
const TRANSLATABLE_TYPES = ['page', 'post', 'article']

exporter par défaut defineConfig({
  document: {
    unstable_languageFilter: (prev, ctx) => {
      const { schemaType, documentId } = ctx

      // Afficher le menu de traduction uniquement pour les vrais documents, pas pour les PTDs
      return TRANSLATABLE_TYPES.includes(schemaType) &&
        documentId &&
        !isPtdId(documentId)
        ? [...prev, DocumentInternationalizationMenu]
        : prev
    },
  },
})

Phrase Configuration

Le plug-in n'a pas de configuration dans l'interface utilisateur de Phrase, mais Phrase doit être configuré pour envoyer des notifications webhook à l'endpoint API backend. Cela permet des mises à jour en temps réel à mesure que les traductions progressent.

Créer un webhook

Créer un webhook avec ces paramètres :

  • URL

    L'URL de l'endpoint API du plug-in, tel que configuré dans l'option apiEndpoint.

  • Événements :

    • Tâches

      • Tâche supprimée

      • Tâche assignée

      • Date d'échéance de la tâche modifiée

      • Cible de la tâche mise à jour

    • Projets

      • Projet supprimé

      • Date d'échéance du projet modifié

    • Autre

      • Pré-traduction terminée

Cela garantit que le plug-in est informé de tout changement dans les projets Phrase et peut maintenir les données de Sanity synchronisées.

Configurer le(s) modèle(s) de projet

Configurer Phrase project template(s) avec les propriétés requises pour les flux de travail et les besoins de l'équipe. Un ou plusieurs modèles peuvent être proposés au choix lors de la commande d'une nouvelle traduction. Les modèles de projet Phrase doivent avoir des paramètres d'importation JSON spécifiques pour que le plug-in fonctionne correctement. Ces paramètres contrôlent quels champs sont envoyés aux traducteurs et lesquels sont préservés en tant que métadonnées.

Importation de fichier JSON

Utiliser des regex pour exclure des clés spécifiques :

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

Cette expression inclut des clés dupliquées intentionnellement pour s'assurer qu'elles sont ignorées par le parseur RegEx de Phrase. Assurez-vous qu'elles sont correctement dupliquées.

  • Exclure les données spécifiques à la localisation, comme la langue d'un document donné si vous utilisez @sanity/document-internationalization.

  • Inclure toutes les clés spécifiques au projet qui ne nécessitent pas de traduction, comme un slug pour le contenu utilisant le même chemin dans toutes les langues. Remplacer YOUR_IGNORED_KEYS_HERE par une liste de clés à ignorer séparées par des pipes.

  • Note de contexte : /_sanityContext

Langue source

Actuellement, ce plug-in fonctionne sur l'hypothèse d'avoir une seule langue source. Le(s) modèle(s) de projet doivent avoir la même source que celle configurée dans la sourceLanguage du plug-in.

Langues cibles

Assurez-vous que les langues choisies dans Phrase sont synchronisées avec celles de la configuration du plug-in.

Point de terminaison de l'API de Sanity

C'est le point de terminaison que le plug-in utilise pour communiquer avec le Sanity Studio. Il est utilisé pour s'authentifier auprès de l'API de Phrase, recevoir des webhooks et des demandes d'utilisateurs depuis le studio Sanity.

Créer un point de terminaison API personnalisé dans le projet Sanity pour gérer ces demandes. L'une des façons les plus simples de le faire est d'utiliser des fonctions sans serveur via des frameworks front-end comme NextJS, Remix, SvelteKit ou Nuxt.

Accéder à la configuration du gestionnaire avec un modèle de demande-réponse via import {createRequestHandler} from sanity-plugin-phrase/backend ou utilisez le gestionnaire interne directement via import {createInternalHandler} from sanity-plugin-phrase/backend. Assurez-vous que les demandes CORS sont gérées correctement lorsque le studio et le point de terminaison ont des origines différentes.

Le répertoire d'app de NextJS n'est actuellement pas pris en charge car il analyse incorrectement le gestionnaire backend comme un composant client React.

Cet exemple démontre la création d'un gestionnaire de route au chemin apiEndpoint configuré à /api/phrase en utilisant le routeur de pages Next.js :

// app/api/phrase/route.ts
// Support de la route API Next.js : https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
importer { PHRASE_CONFIG } depuis 'phraseConfig'
importer { createInternalHandler } depuis 'sanity-plugin-phrase/backend'

importer { writeToken } depuis '~/lib/sanity.api'
importer { client } depuis '~/lib/sanity.client'

exporter const maxDuration = 60
exporter 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,
})

exporter par défaut 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éthode non autorisée' })
    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
// Route API Next.js : https://nextjs.org/docs/pages/building-your-application/routing/api-routes
import type { NextApiRequest, NextApiResponse } from 'next'
importer { PHRASE_CONFIG } depuis 'phraseConfig'
importer { createInternalHandler } depuis 'sanity-plugin-phrase/backend'
importer { writeToken } depuis '~/lib/sanity.api'
importer { client } depuis '~/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,
})

exporter par défaut 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éthode non autorisée' })
    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)
}

adaptateurs i18n

Sanity n'a pas d'approche prescriptive pour l'internationalisation, et il existe de nombreuses façons de l'implémenter. Ce plug-in utilise un modèle d'adaptateur pour permettre la configuration en fonction de la façon dont le contenu est structuré et comment il doit être traduit.

Actuellement, le seul adaptateur disponible est documentInternationalizationAdapter, qui est celui utilisé par le plug-in officiel de Sanity document-internationalization (version ^2.0.0). Déposez un problème si un adaptateur spécifique est requis ou référez-vous à ce référentiel package/src/adapters/document-internationalization.ts pour un exemple sur la façon de mettre en œuvre un personnalisé.

Transformateurs de données personnalisés

Si la transformation des données avant de les envoyer à Phrase est nécessaire, utilisez l'option dataTransformers. Ceci est utile si vous changez la structure des données, ou si vous excluez certains champs de la traduction.

Chaque transformateur de données doit encoder les données avant de les envoyer à Phrase ; et les décoder lors de leur réception pour les transformer avant de les enregistrer dans Sanity. Plusieurs transformateurs peuvent être empilés et exécutés séquentiellement.

Le plug-in n'offre aucun moyen de tester les transformateurs isolément, donc le développement peut être complexe. Enregistrez de vrais documents cibles du jeu de données Sanity au format .JSON et utilisez-les comme données de test pour chacune des fonctions d'encodage/décodage.

Exemple de modification de fichiers VTT encodés en JSON en HTML afin que Phrase puisse mieux segmenter le contenu des sous-titres :

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

const vttJsonTransformer : DataTransformer = {
  encode: {
    array(arr) {
      // Vérifiez si le tableau contient des nœuds de sous-titres 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 // Return undefined to skip transformation
    },
  },
  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 = {
  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
    },
  },
}

// Skipping implementation
// Refer to /demo-nextjs/src/utils/vttJsonTransformer.ts for the full source code
declare function decodeSubtitles(
  encoded: EncodedSubtitles,
) : StoredSubtitleNode[]
declare function encodeSubtitles(nodes: StoredSubtitleNode[]): EncodedSubtitles

Limiter l'accès de l'éditeur

Les éditeurs qui peuvent accéder au tableau de bord Phrase peuvent être limités en implémentant l'option isPhraseDashboardHidden. Cette fonction est équivalente à celle passée à la propriété hidden d'un champ dans Sanity. Elle reçoit un contexte avec l'utilisateur et le document actuels et doit retourner un booléen.

Exemple de limitation de l'accès au tableau de bord Phrase aux utilisateurs ayant le rôle administrateur :

const PHRASE_CONFIG = definePhraseOptions({
  // ...
  isPhraseDashboardHidden: (context) => {
    const isAdmin = (context.currentUser.roles || []).some(
      (r) => r.name === 'admin',
    )
    // Masquer si ce n'est pas un administrateur
    return !estAdministrateur
  },
})
Cet article vous a-t-il été 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.