Le plug-in permet d'accéder au contenu traduit de Phrase depuis le studio Sanity.
Seules les traductions au niveau du document sont prises en charge. Les traductions au niveau du champ ne sont pas prises en charge.
Fonctionnalités :
-
Aperçus en temps réel
Les traductions sont maintenues synchronisées pour permettre aux linguistes et aux traducteurs de voir les modifications de l'aperçu en temps réel.
-
Retraductions intelligentes
Le plug-in compare le contenu qui a été modifié depuis la dernière traduction et n'envoie que ces modifications à Phrase.
-
Traduction automatique des références
Lors de la demande de traductions, les éditeurs peuvent choisir de traduire également les documents référencés par le document actuel et le plug-in les liera automatiquement par langue cible.
-
Schémas flexibles
Quelle que soit la structure, le plug-in s'y adapte et garantit que le contenu traduit final est conforme aux schémas Sanity.
-
Flux de travail Phrase
Les flux de travail de traduction dans Phrase restent les mêmes ; aucune nouvelle formation ou reconfiguration des opérations n'est requise.
L'installation s'effectue à partir de la ligne de commande.
Il est supposé 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 Starter fournis par Sanity.
Installation
Accédez au projet contenant l'instance Sanity Studio et installez le plug-in :
npm install sanity-plug-in-Phrase # ou pnpm, yarn, bun
Variables d'environnement
Avant de configurer le plug-in, les variables d'environnement suivantes doivent être configurées. Créer un fichier `.env` (ou `.env.local` pour Next.js) à la racine du projet.
Les exemples ci-dessous sont donnés pour NextJS. Pour les 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 backend du plug-in sera situé
NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT="http://localhost:3000/api/phrase"
# Région du centre de données 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 autorisations 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 plug-in
Le plug-in est ajouté à sanity.config.ts avec les options de configuration requises :
// sanity.config.ts
import { defineConfig } from 'sanity'
import {
phrasePlugin,
definePhraseOptions,
documentInternationalizationAdapter,
} from 'sanity-plug-in-phrase'
const PHRASE_CONFIG = definePhraseOptions({
// Requis : adaptateur i18n pour l'internationalisation de document
i18nAdapter: documentInternationalizationAdapter(),
// Requis : types de document pouvant être traduits
translatableTypes: ['page', 'post', 'article'],
// Requis : langue source (langue principale)
// Ceci doit correspondre à la langue définie dans votre modèle* de projet Phrase
sourceLang: 'en',
// Requis : langues cible vers lesquelles les utilisateurs peuvent traduire
// Utiliser 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 : URL de votre point de terminaison API backend
apiEndpoint: process.env.NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT!,
// Requis : région du centre de données Phrase ('eu' ou 'us')
phraseRegion: process.env.NEXT_PUBLIC_PHRASE_REGION as 'eu' | 'us',
// Requis : modèles* de projet Phrase disponibles pour les éditeurs
phraseTemplates: [
{
templateUid: 'YOUR_TEMPLATE_UID_HERE',
label: '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 facultatifs
// Profondeur maximale pour la traduction de documents référencés (par défaut : 3)
maxReferencesDepth: 3,
// Autoriser la traduction de documents brouillons (par défaut : false)
translateDrafts: false,
// Transformateurs de données personnalisé pour les types de contenu spéciaux
dataTransformers: [],
// Configuration de la journalisation pour le débogage
logger: {
minimumLogLevel: 'info', // 'debug' | 'info' | 'avertissement' | 'error' | 'fatal'
},
// Masquer le tableau de bord Phrase en fonction des rôles utilisateur
isPhraseDashboardHidden: (context) =>
!(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})
export default defineConfig({
// ... votre configuration existante
plugins: [
phrasePlugin(PHRASE_CONFIG),
// ... autres plug-ins
],
})// sanity.config.(js|ts)
import {
phrasePlugin,
documentInternationalizationAdapter,
} from 'sanity-plug-in-phrase'
const PHRASE_CONFIG = definePhraseOptions({
/**
* L'adaptateur i18n à utiliser pour ce plug-in.
* Il sera responsable de la récupération et de la modification des documents pour chaque cible langue.
*
* Voir ci-dessous pour plus d'informations sur les adaptateurs.
*/
i18nAdapter: documentInternationalizationAdapter(),
/**
* Types de schéma Sanity que le plug-in peut traduire
*/
translatableTypes: ['page', 'post', 'course', 'lesson', 'definition'],
/**
* code de langue de toutes les langues vers lesquelles les utilisateurs peuvent traduire.
* Doit être identique à celui stocké dans vos documents Sanity et utilisé par votre front-end. Le plug-in 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 identique à celui stocké dans vos documents Sanity et utilisé par votre front-end. Le plug-in le traduira automatiquement au format de Phrase.
*/
sourceLang: 'en',
/**
* Tel que défini par les paramètres de votre compte Phrase
* Soit `eu`, soit `us`
*/
phraseRegion: 'us|eu',
/**
* L'URL de votre API de backend de plug-in configurée.
*
* **Remarque :** suivez les étapes de configuration du 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 front-end 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',
label: '[Sanity.io] Modèle par défaut',
},
],
/**
* @optional
* Au cas où vous souhaiteriez afficher ou masquer le tableau de bord Phrase en fonction des privilèges utilisateur.
*
* Reçoit un contexte avec l'utilisateur et le document actuels et doit renvoyer un booléen.
*/
isPhraseDashboardHidden: (context) =>
!(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})
export default defineConfig({
// ...
plugins: [
// ...
phrasePlugin(PHRASE_CONFIG),
],
})
Injection de schéma
Afin d'indiquer au plug-in quels types de document peuvent être traduits, passez un tableau de types de document à la fonction injectPhraseIntoSchema dans le fichier sanity.config.ts :
// sanity.config.ts
import { injectPhraseIntoSchema } from 'sanity-plugin-phrase'
// Liste des types de schéma traduisibles. Habituellement exporté depuis un fichier index
// où que se trouve votre schéma Sanity
const TRANSLATABLE_SCHEMAS = ['page', 'post', 'course', 'lesson', 'definition']
export default defineConfig({
schema: {
types: injectPhraseIntoSchema(TRANSLATABLE_SCHEMAS, PHRASE_CONFIG),
templates: (prev) =>
prev.filter((template) => !TRANSLATABLE_SCHEMAS.includes(template.id)),
},
plugins: [
// ...
phrasePlugin({
// Vos options de configuration ici
}),
],
})
Exclure les Phrase des listes de document
Les Phrase (Phrase Translation Documents) sont des document temporaires qui ne devraient pas apparaître dans les listes de document normales dans Sanity Studio. La constante NOT_PTD fournit un filtre GROQ à cet effet :
// sanity.config.ts
importer { NOT_PTD } depuis 'sanity-plugin-phrase/utils'
export default defineConfig({
// ... autre configuration
plugins: [
structureTool({
structure: (S) =>
S.list()
.title('Content')
.items([
S.listItem()
.title('Posts')
.schemaType('post')
.child(
S.documentList()
.title('Posts')
.filter(`_type == "post" && ${NOT_PTD}`),
),
// ... autres éléments
]),
}),
],
})
Masquer le menu de traduction des Phrase
Lors de l'utilisation du plug-in document-internationalization, le menu de traduction doit être masqué pour les Phrase. L'utilitaire isPtdId identifie les document Phrase :
importer { isPtdId } depuis 'sanity-plugin-phrase/utils'
importer { DocumentInternationalizationMenu } depuis '@sanity/document-internationalization'
// Utiliser le même tableau que translatableTypes depuis votre PHRASE_CONFIG
const TRANSLATABLE_TYPES = ['page', 'post', 'article']
export default defineConfig({
document: {
unstable_languageFilter: (prev, ctx) => {
const { schemaType, documentId } = ctx
// Afficher uniquement le menu de traduction pour les documents réels, pas pour les PTD
return TRANSLATABLE_TYPES.includes(schemaType) &&
documentId &&
!isPtdId(documentId)
? [...prev, DocumentInternationalizationMenu]
: prev
},
},
})
Le plug-in n'a aucune configuration dans l'interface utilisateur Phrase, mais Phrase doit être configuré pour envoyer des notifications webhook au point de terminaison de l'API backend. Cela permet des mises à jour en temps réel à mesure que la progression des traductions avance.
Créer un webhook
Créer un webhook avec ces paramètres :
-
URL
L'URL vers le point de terminaison de l'API du plug-in, tel que configuré dans l'option .
-
É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 toute modification apportée aux projets Phrase et peut maintenir les données Sanity synchronisées.
Configuration des modèle* de projet
Configurez les modèle* de projet Phrase avec les propriétés requises pour les flux de travail et les exigences de la Team. Un ou plusieurs modèles peuvent être proposés au choix lors de la commande d'une nouvelle traduction. Les modèle* de projet Phrase doivent avoir des paramètres d'import spécifiques pour que le plug-in fonctionne correctement. Ces paramètres contrôlent quels champs sont envoyés aux traducteurs et lesquels sont conservés en tant que métadonnées.
Import de fichier JSON
Utiliser une regex pour exclure des clés spécifiques :
(^|.*\/) (_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) /.*)
Cette expression inclut des clés dupliquées à dessein pour garantir qu'elles sont ignorées par l'analyseur RegEx de Phrase. S'assurer 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_HEREpar une liste de clés à ignorer séparées par des barres verticales. -
Note de contexte :
/_sanityContext
Langue source
Actuellement, ce plug-in fonctionne en supposant qu'il n'y a qu'une seule langue source. Le ou les modèles de projet doivent avoir la même source que celle configurée dans la sourceLanguage du plug-in.
Langues cibles
S'assurer que les langues choisies dans Phrase sont synchronisées avec celles de la configuration du plug-in.
Il s'agit de l'endpoint que le plug-in utilise pour communiquer avec Sanity Studio. Il est utilisé pour s'authentifier auprès de l'API de Phrase, recevoir des webhooks et des requêtes utilisateur depuis Sanity Studio.
Créer un API endpoint Personnalisé dans le projet pour gérer ces requêtes. L'un des moyens les plus simples pour ce faire est d'utiliser des fonctions serverless via des frameworks front-end comme NextJS, Remix, SvelteKit ou Nuxt.
Accéder à la configuration du gestionnaire avec un modèle Requête-Réponse via import {createRequestHandler} from ou utiliser le gestionnaire interne directement via import {createInternalHandler} from . Assurez-vous que les requêtes CORS sont gérées correctement lorsque le studio et le point de terminaison ont des origines différentes.
Le répertoire app de NextJS n'est pas actuellement 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 route API Next.js : https://nextjs.org/docs/api-routes/introduction
importer type { NextApiRequest, NextApiResponse } from 'next'
importer { PHRASE_CONFIG } from 'phraseConfig'
importer { createInternalHandler } from 'sanity-plug-in-phrase/backend'
import { writeToken } from '~/lib/sanity.api'
importer { client } from '~/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 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.statut(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: [chaîne, any]) => {
res.setHeader(value[0], value[1])
})
res.status(phraseRes.status).json(resBody)
}// src/pages/api/phrase.ts
// Route d'API Next.js : https://nextjs.org/docs/pages/building-your-application/routing/api-routes
importer type { NextApiRequest, NextApiResponse } from 'next'
importer { PHRASE_CONFIG } from 'phraseConfig'
importer { createInternalHandler } from 'sanity-plug-in-phrase/backend'
import { writeToken } from '~/lib/sanity.api'
importer { client } from '~/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 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.statut(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 de l'internationalisation, et il existe de nombreuses façons de la mettre en œuvre. Ce plug-in utilise un modèle d'adaptateur pour permettre une configuration basée sur la façon dont le contenu est structuré et dont il doit être traduit.
Actuellement, le seul adaptateur disponible est , qui est celui utilisé par le plug-in officiel document-internationalization plugin de Sanity (version ^2.0.0). Signalez un problème si un adaptateur spécifique est requis ou consultez ce référentiel package/src/adapters/document-internationalization.ts pour un exemple sur la façon d'en implémenter un personnalisé.
Transformateurs de données personnalisé
Si la transformation des données avant leur envoi à Phrase est requise, utiliser l'option . Ceci est utile pour modifier la structure des données ou pour exclure 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 de manière isolée, le développement peut donc être complexe. Enregistrer les documents cible réels du jeu de données Sanity au format .JSON et les utiliser comme données de test pour chaque fonction d'encodage/décodage.
Exemple de modification de fichiers VTT encodés en JSON vers 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érifier 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 === 'chaîne' &&
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 === 'chaîne' &&
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 pouvant accéder au tableau de bord Phrase peuvent être limités en implémentant l'option . Cette fonction est équivalente à celle transmise à la propriété hidden d'un champ dans Sanity. Elle reçoit un contexte avec l'utilisateur et le document actuels et doit renvoyer 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 === 'administrateur',
)
// Masquer si l'utilisateur n'est pas administrateur
return !isAdmin
},
})