Il plugin fornisce l'accesso al contenuto tradotto di Phrase dall'interno di Sanity studio.
Sono supportate solo le traduzioni a livello di documento. Le traduzioni a livello di campo non sono supportate.
Funzionalità:
-
Anteprima in tempo reale
Le traduzioni vengono mantenute sincronizzate per consentire a linguisti e traduttori di vedere le modifiche all'anteprima in tempo reale.
-
Ritraduzioni intelligenti
Il plugin rileva le differenze nel contenuto modificato dall'ultima traduzione e invia solo tali modifiche a Phrase.
-
Traduzione automatica dei riferimenti
Quando richiedono traduzioni, gli editor possono scegliere di tradurre anche i documenti referenziati da quello corrente e il plugin li collegherà automaticamente in base alla lingua di destinazione.
-
Schemi flessibili
Indipendentemente dalla struttura, il plugin si adatta ad essa e garantisce che il contenuto tradotto finale sia conforme agli schemi di Sanity.
-
Flussi di lavoro Phrase
I flussi di lavoro di traduzione in Phrase rimangono invariati; non è richiesta alcuna riqualificazione o riconfigurazione delle operazioni.
L'installazione viene eseguita dalla riga di comando.
Si presuppone che un generatore di siti web e Sanity Studio siano già configurati. In caso contrario, usare uno dei Starter template forniti da Sanity.
Installazione
Passare al progetto contenente l'istanza di Sanity Studio e installare il plugin:
npm install sanity-plugin-phrase # o pnpm, yarn, bun
Variabili d'ambiente
Prima di configurare il plugin, è necessario configurare le seguenti variabili d'ambiente. Creare un file `.env` (o `.env.local` per Next.js) nella root del progetto.
Gli esempi seguenti 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 cliente. In Next.js, solo le variabili con il prefisso NEXT_PUBLIC_ vengono esposte al browser.
# URL di base del sito (utilizzato per i link di anteprima)
NEXT_PUBLIC_BASE_URL="http://localhost:3000"
# URL in cui si troverà il gestore backend del plugin
NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT="http://localhost:3000/api/phrase"
# Regione del datacenter 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 Sanity con autorizzazioni di scrittura (solo lato server)
SANITY_WRITE_TOKEN=""
# Credenziali Phrase (solo lato server)
# Nota: l'API Phrase richiede solo la parte del nome utente, NON l'indirizzo e-mail completo
PHRASE_USER_NAME="phraseUsername"
PHRASE_PASSWORD="secretPassword"
Configurazione plugin
Il plugin viene aggiunto a sanity.config.ts con le opzioni di configurazione richieste:
// sanity.config.ts
importare { defineConfig } from 'sanity'
importare {
phrasePlugin,
definePhraseOptions,
documentInternationalizationAdapter,
} from 'sanity-plugin-phrase'
const PHRASE_CONFIG = definePhraseOptions({
// Richiesto: adattatore i18n per l'internazionalizzazione del documento
i18nAdapter: documentInternationalizationAdapter(),
// Richiesto: Tipi di documento traducibili
translatableTypes: ['page', 'post', 'article'],
// Richiesto: Lingua di origine (lingua primaria)
// Questo deve corrispondere alla lingua definita nel modello di progetto Phrase
sourceLang: 'en',
// Richiesto: Lingue di destinazione in cui gli utenti possono tradurre
// Usare gli stessi codici dei documenti Sanity
// Questo elenco deve corrispondere alle lingue definite nel modello di progetto Phrase
supportedTargetLangs: ['es', 'fr', 'de', 'pt'],
// Richiesto: URL dell'endpoint API di backend
apiEndpoint: process.env.NEXT_PUBLIC_PHRASE_plugin_API_ENDPOINT!,
// Obbligatorio: regione del datacenter Phrase ('eu' o 'us')
phraseRegion: process.env.NEXT_PUBLIC_PHRASE_REGION as 'eu' | 'us',
// Obbligatorio: modelli di progetto Phrase disponibili per gli editor
phraseTemplates: [
{
templateUid: 'YOUR_TEMPLATE_UID_HERE',
label: '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 facoltative
// Profondità massima per la traduzione di documenti referenziati (predefinito: 3)
maxReferencesDepth: 3,
// Consenti la traduzione di documenti in bozza (predefinito: false)
translateDrafts: false,
// Trasformatori di dati personalizzati per tipi di contenuto speciali
dataTransformers: [],
// Configurazione di registrazione per il debug
logger: {
minimumLogLevel: 'info', // 'debug' | 'info' | 'avviso' | 'error' | 'fatal'
},
// Nascondi dashboard Phrase in base ai ruoli utente
isPhraseDashboardHidden: (context) =>
!(context.currentUser.roles || []).some((r) => r.name === 'amministratore'),
})
export default defineConfig({
// ... la tua configurazione esistente
plugins: [
phrasePlugin(PHRASE_CONFIG),
// ... altri plugin
],
})// sanity.config.(js|ts)
importare {
phrasePlugin,
documentInternationalizationAdapter,
} from 'sanity-plugin-phrase'
const PHRASE_CONFIG = definePhraseOptions({
/**
* L'adattatore i18n da usare per questo plugin.
* Sarà responsabile del recupero e della modifica dei documenti per ogni lingua di destinazione.
*
* Vedere di seguito per ulteriori informazioni sugli adattatori.
*/
i18nAdapter: documentInternationalizationAdapter(),
/**
* Tipi di schema Sanity che il plugin può tradurre
*/
translatableTypes: ['page', 'post', 'course', 'lesson', 'definition'],
/**
* codice della lingua di tutte le lingue in cui gli utenti possono tradurre.
* Dovrebbe essere lo stesso memorizzato nei documenti Sanity e usato dal front-end. Il plugin lo tradurrà automaticamente nel formato di Phrase.
*/
supportedTargetLangs: ['cz', 'es', 'pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru'],
/**
* codice della lingua della lingua di origine che verrà tradotta.
* Dovrebbe essere lo stesso memorizzato nei documenti Sanity e usato dal front-end. Il plugin lo tradurrà automaticamente nel formato di Phrase.
*/
sourceLang: 'en',
/**
* Come definito dalle impostazioni dell'account Phrase
* O `eu` o `us`
*/
phraseRegion: 'us|eu',
/**
* L'URL del backend API del plugin configurato.
*
* **Nota:** seguire i passaggi per la configurazione dell'endpoint, descritti di seguito
*/
apiEndpoint: 'https://my-site.com/api/phrase',
/**
* Usato per reindirizzare i linguisti dal dashboard 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 usare quando richiedono traduzioni.
*
* **Nota:** seguire i passaggi per l'impostazione dei modelli, descritti di seguito
*/
phraseTemplates: [
{
templateUid: '1jYg0Pc1d8kAHUyM0tgdmt',
label: '[Sanity.io] Modello predefinito',
},
],
/**
* Facoltativo
* Nel caso in cui si desideri mostrare o nascondere il dashboard Phrase in base ai privilegi dell'utente.
*
* Riceve un contesto con l'utente e il documento correnti e deve restituire un valore booleano.
*/
isPhraseDashboardHidden: (context) =>
!(context.currentUser.roles || []).some((r) => r.name === 'amministratore'),
})
export default defineConfig({
// ...
plugins: [
// ...
phrasePlugin(PHRASE_CONFIG),
],
})
Iniezione dello schema
Per indicare al plugin quali tipi di documento possono essere tradotti, passare una matrice 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 traducibile. Solitamente esportato da un file indice
// ovunque si trovi il tuo Sanity Schema
const TRANSLATABLE_SCHEMAS = ['page', 'post', 'course', 'lesson', 'definition']
export default defineConfig({
schema: {
types: injectPhraseIntoSchema(TRANSLATABLE_SCHEMAS, PHRASE_CONFIG),
templates: (prev) =>
prev.filtrare((modello) => !TRANSLATABLE_SCHEMAS.includes(modello.ID)),
},
plugins: [
// ...
phrasePlugin({
// Le tue opzioni di configurazione qui
}),
],
})
Esclusione dei PTD dagli elenchi di documento
I PTD (Phrase Translation Documents) sono documento temporanei che non dovrebbero apparire negli elenchi di documento normali all'interno di Sanity Studio. La costante NOT_PTD fornisce un filtro GROQ a questo scopo:
// sanity.config.ts
importare { NOT_PTD } from 'sanity-plugin-phrase/utils'
export default defineConfig({
// ... altra configurazione
plugins: [
structureTool({
structure: (S) =>
S.list()
.title('Content')
.items([
S.listItem()
.title('Posts')
.schemaType('post')
.child(
S.documentList()
.title('Posts')
.filtrare(`_type == \"post\" && ${NOT_PTD}`),
),
// ... altri elementi
]),
}),
],
})
Nascondere il menu di traduzione dai PTD
Quando si usa il plugin document-internationalization, il menu di traduzione dovrebbe essere nascosto dai PTD. L'utility isPtdId identifica i documenti PTD:
importare { isPtdId } from 'sanity-plugin-phrase/utils'
importare { DocumentInternationalizationMenu } from '@sanity/document-internationalization'
// Usare lo stesso array di translatableTypes dal proprio PHRASE_CONFIG
const TRANSLATABLE_TYPES = ['page', 'post', 'article']
export default defineConfig({
documento: {
unstable_languageFilter: (prev, ctx) => {
const { schemaType, documentId } = ctx
// Mostrare il menu di traduzione solo per i documenti reali, non per i PTD
return TRANSLATABLE_TYPES.includes(schemaType) &&
documentId &&
!isPtdId(documentId)
? [...prev, DocumentInternationalizationMenu]
: prev
},
},
})
Il plugin non ha alcuna configurazione nell'interfaccia utente di Phrase, ma Phrase deve essere configurato per inviare notifiche webhook all'endpoint dell'API di backend. Ciò consente aggiornamenti in tempo reale man mano che le traduzioni procedono con l'avanzamento.
Creare un webhook
Creare un webhook con queste impostazioni:
-
URL
L'URL dell'endpoint API del plugin, come configurato nell'opzione .
-
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
-
-
Ciò garantisce che il plugin venga notificato di eventuali modifiche ai progetti Phrase e possa mantenere sincronizzati i dati di Sanity.
Configurazione modello/i di progetto
Configurare il/i modello/i di progetto Phrase project template(s) con le proprietà richieste per i flussi di lavoro e i requisiti del Team. È possibile offrire uno o più modelli tra cui scegliere quando si ordina una nuova traduzione. I modello di progetto Phrase devono avere impostazioni di importare JSON specifiche affinché il plugin funzioni correttamente. Queste impostazioni controllano quali campi vengono inviati ai traduttori e quali vengono conservati come metadati.
Importare file JSON
Usare regex per escludere chiavi specifiche:
(^|.*\/) (_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) /.*)
Questa espressione include chiavi duplicate di proposito per garantire che vengano ignorate dal parser RegEx di Phrase. Assicurarsi che siano duplicate correttamente.
-
Escludere i dati specifici della localizzazione, come la lingua di un dato documento se si utilizza
@sanity/document-internationalization. -
Includere eventuali chiavi specifiche del progetto che non richiedono traduzione, come uno slug per il contenuto che utilizza lo stesso percorso in tutte le lingue. Sostituire
YOUR_IGNORED_KEYS_HEREcon un elenco separato da pipe di chiavi da ignorare. -
Nota di contesto:
/_sanityContext
Lingua di origine
Attualmente, questo plugin opera partendo dal presupposto di avere una singola lingua di origine. Il/i modello/i di progetto deve/devono avere la stessa origine di quella configurata nel sourceLanguage del plugin.
Lingue di destinazione
Assicurarsi che le lingue scelte in Phrase siano sincronizzate con ciò che è presente nella configurazione del plugin.
Questo è l'endpoint che il plugin usa per comunicare con Sanity Studio. Viene usato per autenticarsi all'API di Phrase, ricevere webhook e richieste utente dallo studio Sanity.
Creare un endpoint API Personalizzato nel progetto Sanity per gestire queste richieste. Uno dei modi più semplici per farlo è usare funzioni serverless tramite framework front-end come NextJS, Remix, SvelteKit o Nuxt.
Accedere configurare il gestore con un modello Request-Response tramite import {createRequestHandler} da o usare il gestore interno direttamente tramite import {createInternalHandler} da . Assicurarsi che le richieste CORS siano gestite correttamente, lo studio e l'endpoint hanno origini diverse.
La directory app di NextJS non è attualmente supportata poiché analizza erroneamente il gestore backend come un componente client React.
Questo esempio dimostra la creazione di un Route Handler nel percorso API configurato apiEndpoint presso /api/phrase usando il Pages Router di Next.js:
// app/api/phrase/route.ts
// Supporto per 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'
importare { writeToken } from '~/lib/sanity.API'
importare { cliente } from '~/lib/sanity.cliente'
esportare const maxDuration = 60
esportare const dynamic = 'force-dynamic'
const PhraseHandler = createInternalHandler({
PhraseCredentials: {
userName: process.env.Phrase_utente_NAME || '',
password: process.env.Phrase_PASSWORD || '',
},
sanitycliente: cliente.withConfig({ token: writeToken }),
pluginOptions: Phrase_CONFIG,
})
esportare default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
res.setHeader('accedere-Control-Allow-Origin', '*')
res.setHeader('accedere-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('accedere-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.stato(405).json({ error: 'Method not allowed' })
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: [stringa, 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
import type { NextApiRequest, NextApiResponse } from 'next'
import { PHRASE_CONFIG } from 'phraseConfig'
import { createInternalHandler } from 'sanity-plugin-phrase/backend'
importare { writeToken } from '~/lib/sanity.API'
importare { cliente } from '~/lib/sanity.cliente'
const PhraseHandler = createInternalHandler({
PhraseCredentials: {
userName: process.env.Phrase_utente_NAME || '',
password: process.env.Phrase_PASSWORD || '',
},
sanitycliente: cliente.withConfig({ token: writeToken }),
pluginOptions: Phrase_CONFIG,
})
esportare default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
res.setHeader('accedere-Control-Allow-Origin', '*')
res.setHeader('accedere-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('accedere-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.stato(405).json({ error: 'Method not allowed' })
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 implementarla. Questo plugin usa un pattern adattatore per consentire la configurazione in base a come il contenuto è strutturato e come dovrebbe essere tradotto.
Attualmente, l'unico adattatore disponibile è , che è quello usato dal plugin document-internationalization ufficiale di Sanity (versione ^2.0.0). Apri un problema se è richiesto uno specifico adattatore o fai riferimento a questo repository's package/src/adapters/document-internationalization.ts per un esempio su come implementarne uno personalizzato.
Trasformatori di dati personalizzato
Se è richiesta la trasformazione dei dati prima di inviarli a Phrase, usare l'opzione . 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 su Sanity. È possibile impilare più trasformatori ed eseguirli in sequenza.
Il plugin non offre alcun modo per testare i trasformatori in isolamento, quindi lo sviluppo può essere complesso. Salvare i documenti di destinazione reali dal dataset Sanity in .JSON e usarli come dati di test per ciascuna funzione di encode/decode.
Esempio di modifica di file VTT codificati in JSON in HTML affinché Phrase possa segmentare meglio il contenuto dei sottotitoli:
import { DataTransformer } from 'sanity-plugin-phrase'
const vttJsonTransformer: DataTransformer = {
encode: {
array(arr) {
// Controllare se l'array contiene nodi sottotitolo 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 // Restituire undefined per saltare la trasformazione
},
},
decode: {
object(obj) {
if (!!obj && '_type' in obj && obj._type === 'encodedSubtitles') {
return decodeSubtitles(obj as EncodedSubtitles)
}
return undefined
},
},
}
esportare const PHRASE_CONFIG = definePhraseOptions({
// ...
dataTransformers: [vttJsonTransformer],
})esportare 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
// Fare riferimento a /demo-nextjs/src/utils/vttJsonTransformer.ts per il codice sorgente di origine completo
declare function decodeSubtitles(
encoded: EncodedSubtitles,
): StoredSubtitleNode[]
declare function encodeSubtitles(nodes: StoredSubtitleNode[]): EncodedSubtitles
Limitare l'accesso all'editor
Quali editor possono accedere alla dashboard Phrase può essere limitato implementando l'opzione . Questa funzione è equivalente a quella passata alla proprietà hidden di un campo in Sanity. Riceve un contesto con l'utente e il documento correnti e deve restituire un booleano.
Esempio di limitazione dell'accesso alla dashboard Phrase agli utenti con il ruolo di amministratore:
const PHRASE_CONFIG = definePhraseOptions({
// ...
isPhraseDashboardHidden: (context) => {
const isAdmin = (context.currentUser.roles || []).some(
(r) => r.name === 'admin',
)
// Nascondi se non è un amministratore
return !isAdmin
},
})