Integrace

Sanity (TMS)

Obsah je strojově přeložen z angličtiny s použitím Phrase Language AI.

Plugin poskytuje přístup k přeloženému obsahu Phrase přímo ze studia Sanity.

Podporovány jsou pouze překlady na úrovni dokumentu. Překlady na úrovni pole nejsou podporovány.

Funkce:

  • Náhledy v reálném čase

    Překlady jsou synchronizovány, aby lingvisté a překladatelé mohli vidět změny v náhledech v reálném čase.

  • Inteligentní opětovné překlady

    Plugin porovnává, jaký obsah se od posledního překladu změnil, a pouze tyto změny odesílá do Phrase.

  • Automatické překlady referencí

    Při vydávání překladů mohou editoři zvolit také překlad dokumentů, na které se odkazuje, a plugin je automaticky propojí podle cílového jazyka.

  • Flexibilní schémata

    Bez ohledu na strukturu se plugin přizpůsobí a zajistí, že konečný přeložený obsah odpovídá schématům Sanity.

  • Pracovní postupy Phrase

    Pracovní postupy překladu v Phrase zůstávají stejné; není nutné opětovné školení nebo přeconfigurování operací.

Instalace pluginu Sanity

Instalace se provádí v příkazovém řádku.

Předpokládá se, že generátor webových stránek a Sanity Studio jsou již nakonfigurovány. Pokud ne, použijte jednu z starter šablon poskytnutých Sanity.

Instalace

Přejděte do projektu obsahujícího instanci Sanity Studio a nainstalujte plugin:

npm install sanity-plugin-phrase

# nebo pnpm, yarn, bun

Proměnné prostředí

Před konfigurací pluginu musí být nastaveny následující proměnné prostředí. Vytvořte soubor `.env` (nebo `.env.local` pro Next.js) v kořenovém adresáři projektu.

Příklady níže jsou uvedeny pro NextJS. Pro jiné frameworky se odkažte na jejich specifickou dokumentaci a vezměte na vědomí, že prefix `NEXT_PUBLIC_` může být nutné odstranit pro veřejné proměnné.

Důležité

Proměnné na straně serveru (SANITY_WRITE_TOKEN, PHRASE_USER_NAME, PHRASE_PASSWORD) by nikdy neměly být vystaveny klientovi. V Next.js jsou vystaveny pouze proměnné s prefixem NEXT_PUBLIC_ prohlížeči.

# Základní URL vaší stránky (používá se pro náhledové odkazy)
NEXT_PUBLIC_BASE_URL="http://localhost:3000"

# URL, kde se nachází backendový handler pluginu
NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT="http://localhost:3000/api/phrase"

# Region datacentra Phrase ('eu' nebo 'us')
NEXT_PUBLIC_PHRASE_REGION="eu"

# Konfigurace projektu Sanity
NEXT_PUBLIC_SANITY_PROJECT_ID="vaše-id-projektu"
NEXT_PUBLIC_SANITY_DATASET="production"

# API token Sanity s oprávněními pro zápis (pouze na straně serveru)
SANITY_WRITE_TOKEN=""

# Přihlašovací údaje Phrase (pouze na straně serveru)
# Poznámka: API Phrase očekává pouze část uživatelského jména, NE celou e-mailovou adresu
PHRASE_USER_NAME="phraseUsername"
HESLO_FRAZE="tajneHeslo"

Konfigurace pluginu

Plugin je přidán do sanity.config.ts s požadovanými konfiguračními možnostmi:

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

const PHRASE_CONFIG = definePhraseOptions({
  // Povinné: i18n adaptér pro mezinárodní dokumentaci
  i18nAdapter: documentInternationalizationAdapter(),

  // Required: Typy dokumentů, které mohou být přeloženy
  translatableTypes: ['page', 'post', 'article'],

  // Required: Zdrojový jazyk (primární jazyk)
  // To musí odpovídat jazyku definovanému ve vaší šabloně projektu Phrase
  sourceLang: 'en',

  // Required: Cílové jazyky, do kterých mohou uživatelé překládat
  // Použijte stejné kódy jako vaše dokumenty Sanity
  // Tento seznam musí odpovídat jazykům definovaným ve vaší šabloně projektu Phrase
  podporované cílové jazyky: ['es', 'fr', 'de', 'pt'],

  // Required: URL adresa vašeho backend API koncového bodu
  apiEndpoint: process.env.NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT!,

  // Required: Region datacentra Phrase ('eu' nebo 'us')
  phraseRegion: process.env.NEXT_PUBLIC_PHRASE_REGION jako 'eu' | 'us',

  // Required: Šablony projektů Phrase dostupné editorům
  phraseTemplates: [
    {
      templateUid: 'YOUR_TEMPLATE_UID_HERE',
      štítek: 'Výchozí šablona překladu',
    },
  ],

  // Required: Generovat náhledové URL pro lingvisty
  getDocumentPreview: (doc, sanityClient) => {
    const publishedId = doc._id.replace('drafts.', '')
    return `${process.env.NEXT_PUBLIC_BASE_URL}/api/draft?id=${publishedId}`
  },

  // Volitelné nastavení

  // Maximální hloubka pro překlad odkazovaných dokumentů (výchozí: 3)
  maxReferencesDepth: 3,

  // Allow translation of draft documents (default: false)
  translateDrafts: false,

  // Vlastní transformátory dat pro speciální typy obsahu
  dataTransformers: [],

  // Konfigurace protokolování pro ladění
  logger: {
    minimumLogLevel: 'info', // 'debug' | 'info' | 'warning' | 'error' | 'fatal'
  },

  // Skrýt Phrase hlavní panel na základě rolí uživatelů
  isPhraseDashboardHidden: (context) =>
    !(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})

export default defineConfig({
  // ... vaše stávající konfigurace
  plugins: [
    phrasePlugin(PHRASE_CONFIG),
    // ... další pluginy
  ],
})// sanity.config.(js|ts)
import {
  phrasePlugin,
  documentInternationalizationAdapter,
} from 'sanity-plugin-phrase'

const PHRASE_CONFIG = definePhraseOptions({
  /**
   * I18n adaptér, který se má použít pro tento plugin.
   * Bude zodpovědný za načítání a úpravu dokumentů pro každý cílový jazyk.
   *
   * Viz níže pro více informací o adaptérech.
   */
  i18nAdapter: documentInternationalizationAdapter(),

  /**
   * Typy schémat Sanity, které může plugin překládat
   */
  translatableTypes: ['page', 'post', 'course', 'lesson', 'definition'],

  /**
   * Kód jazyka všech jazyků, do kterých mohou uživatelé překládat.
   * Měl by být stejný jako ten, který je uložen ve vašich dokumentech Sanity a používán vaším front-endem. Plugin to automaticky přeloží do formátu Phrase.
   */
  supportedTargetLangs: ['cz', 'es', 'pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru'],

  /**
   * Kód jazyka zdrojového jazyka, který bude přeložen.
   * Měl by být stejný jako ten, který je uložen ve vašich dokumentech Sanity a používán vaším front-endem. Plugin to automaticky přeloží do formátu Phrase.
   */
  sourceLang: 'en',

  /**
   * Jak je definováno v nastavení vašeho účtu Phrase
   * Buď `eu` nebo `us`
   */
  phraseRegion: 'us|eu',

  /**
   * URL vašeho nakonfigurovaného plugin backend API.
   *
   * **Poznámka:** postupujte podle kroků pro nastavení koncového bodu, uvedených níže
   */
  apiEndpoint: 'https://my-site.com/api/phrase',

  /**
   * Používá se k přesměrování lingvistů z hlavního panelu Phrase na náhled jejich překladů na front-endu.
   */
  getDocumentPreview: async (doc, sanityClient) => {
    const publishedId = doc._id.replace('drafts.', '')
    return `${process.env.NEXT_PUBLIC_FRONT_END_URL}/api/draft?publishedId=${publishedId}`
  },

  /**
   * Šablony projektů Phrase, které mohou vaši editoři použít při žádosti o překlady.
   *
   * **Poznámka:** postupujte podle kroků pro nastavení šablon, uvedených níže
   */
  phraseTemplates: [
    {
      templateUid: '1jYg0Pc1d8kAHUyM0tgdmt',
      label: '[Sanity.io] Výchozí šablona',
    },
  ],

  /**
   * @volitelné
   * V případě, že chcete zobrazit nebo skrýt hlavní panel Phrase podle oprávnění uživatelů.
   *
   * Přijímá kontext s aktuálním uživatelským účtem a dokumentem a musí vrátit boolean.
   */
  isPhraseDashboardHidden: (context) =>
    !(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})

export default defineConfig({
  // ...
  plugins: [
    // ...
    phrasePlugin(PHRASE_CONFIG),
  ],
})

Konfigurace pluginu Sanity

Vložení schématu

Abychom pluginu řekli, které typy dokumentů mohou být přeloženy, předáme pole typů dokumentů funkci injectPhraseIntoSchema v souboru sanity.config.ts:

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

// List of translatable schema types. Obvykle exportováno z indexového souboru
// kdekoli máte umístěno své schéma Sanity
const TRANSLATABLE_SCHEMAS = ['page', 'post', 'course', 'lesson', 'definition']

export default defineConfig({
  schema: {
    typy: injectPhraseIntoSchema(TRANSLATABLE_SCHEMAS, PHRASE_CONFIG),
    šablony: (prev) =>
      prev.filter((template) => !TRANSLATABLE_SCHEMAS.includes(template.id)),
  },
  plugins: [
    // ...
    phrasePlugin({
      // Vaše možnosti konfigurace zde
    }),
  ],
})

Vyloučení PTD z dokumentových seznamů

PTD (Dokumenty pro překlad frází) jsou dočasné dokumenty, které by se neměly objevovat v normálních dokumentových seznamech uvnitř Sanity Studio. Konstanta NOT_PTD poskytuje GROQ filtr pro tento účel:

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

export default defineConfig({
  // ... další konfigurace
  plugins: [
    structureTool({
      struktura: (S) =>
        S.list()
          .title('Content')
          .items([
            S.listItem()
              .title('Posts')
              .schemaType('post')
              .child(
                S.documentList()
                  .title('Posts')
                  .filter(`_type == "post" && ${NOT_PTD}`),
              ),
            // ... další položky
          ]),
    }),
  ],
})

Skrýt překladatelské menu pro PTD

Při používání pluginu pro mezinárodní dokumentaci by mělo být překladatelské menu skryto pro PTD. Nástroj isPtdId identifikuje PTD dokumenty:

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

// Použijte stejné pole jako translatableTypes z vašeho PHRASE_CONFIG
const TRANSLATABLE_TYPES = ['stránka', 'příspěvek', 'článek']

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

      // Zobrazit překladatelské menu pouze pro skutečné dokumenty, ne pro PTD
      return TRANSLATABLE_TYPES.includes(schemaType) &&
        documentId &&
        !isPtdId(documentId)
        ? [...prev, DocumentInternationalizationMenu]
        : prev
    },
  },
})

Konfigurace fráze

Plugin nemá žádnou konfiguraci v uživatelském rozhraní fráze, ale fráze musí být nakonfigurována pro odesílání webhookových oznámení na koncový bod API na pozadí. To umožňuje aktualizace v reálném čase, jak postupují překlady.

Vytvořit webhook

Vytvořte webhook s těmito nastaveními:

  • URL

    URL koncového bodu API pluginu, jak je nakonfigurováno v možnosti apiEndpoint.

  • Události:

    • Zakázky

      • Zakázka odstraněna

      • Job assigned

      • Termín dodání zakázky změněn

      • Cíl zakázky aktualizován

    • Projekty

      • Projekt odstraněn

      • Termín dodání projektu změněn

    • Jiné

      • Předpřeklad dokončen

To zajišťuje, že plugin je informován o jakýchkoli změnách v projektech fráze a může udržovat data Sanity synchronizovaná.

Nastavení šablony projektu

Nakonfigurujte frázi šablonu projektu s vlastnostmi požadovanými pro pracovní postupy a požadavky týmu. Jedna nebo více šablon může být nabídnuta k výběru při objednávání nového překladu. Šablony projektů fráze musí mít specifická nastavení importu JSON, aby plugin fungoval správně. Tato nastavení řídí, které pole jsou odesílána překladatelům a která jsou uchovávána jako metadata.

Import JSON souboru

Použijte regex k vyloučení specifických klíčů:

(^|.*\/)
(_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|VAŠE_VYLOUČENÉ_KLÍČE_TADY|
(_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|VAŠE_VYLOUČENÉ_KLÍČE_TADY)
/.*)

Tento výraz zahrnuje duplicitní klíče záměrně, aby se zajistilo, že budou ignorovány parserem RegEx fráze. Zajistěte, aby byly správně duplikovány.

  • Vyloučit lokalizačně specifická data, jako je jazyk daného dokumentu, pokud používáte @sanity/document-internationalization.

  • Zahrnout jakékoli klíče specifické pro projekt, které nevyžadují překlad, jako je slug pro obsah používající stejnou cestu ve všech jazycích. Nahradit YOUR_IGNORED_KEYS_HERE pipe-separovaným seznamem klíčů k ignorování.

  • Kontextová poznámka: /_sanityContext

Zdrojový jazyk

V současnosti tento plugin funguje na základě předpokladu, že má jediný zdrojový jazyk. Šablona projektu musí mít stejný zdroj jako ten, který je nakonfigurován v pluginu sourceLanguage.

Cílové jazyky

Ujistěte se, že jazyky vybrané v Phrase jsou v souladu s tím, co je v konfiguraci pluginu.

Sanity apiEndpoint

Toto je koncový bod, který plugin používá k komunikaci se Sanity Studio. Používá se k autentizaci k API Phrase, přijímání webhooků a uživatelských požadavků ze studia Sanity.

Vytvořte vlastní API koncový bod v projektu Sanity pro zpracování těchto požadavků. Jedním z nejjednodušších způsobů, jak to udělat, je použít serverless funkce prostřednictvím front-endových rámců jako NextJS, Remix, SvelteKit nebo Nuxt.

Přístup k nakonfigurování obsluhy s vzorem požadavek-odpověď prostřednictvím import {createRequestHandler} z sanity-plugin-phrase/backend nebo použijte interní obsluhu přímo prostřednictvím import {createInternalHandler} z sanity-plugin-phrase/backend. Ujistěte se, že požadavky CORS jsou správně zpracovány, studio a koncový bod mají různé původy.

Adresář aplikace NextJS v současnosti není podporován, protože nesprávně analyzuje backendovou obsluhu jako komponentu React klienta.

Tento příklad demonstruje vytvoření obsluhy trasy na nakonfigurované apiEndpoint cestě na /api/phrase pomocí Next.js Pages Router:

// app/api/phrase/route.ts
// Podpora API trasy Next.js: https://nextjs.org/docs/api-routes/introduction
importovat typ { NextApiRequest, NextApiResponse } z 'next'
importovat { PHRASE_CONFIG } z 'phraseConfig'
importovat { createInternalHandler } z 'sanity-plugin-phrase/backend'

importovat { writeToken } z '~/lib/sanity.api'
importovat { klient } z '~/lib/sanity.client'

stáhnout const maxDuration = 60
stáhnout const dynamic = 'force-dynamic'

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

stáhnout 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: 'Metoda není povolena' })
    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
// Next.js API route: https://nextjs.org/docs/pages/building-your-application/routing/api-routes
importovat typ { NextApiRequest, NextApiResponse } z 'next'
importovat { PHRASE_CONFIG } z 'phraseConfig'
importovat { createInternalHandler } z 'sanity-plugin-phrase/backend'
importovat { writeToken } z '~/lib/sanity.api'
importovat { klient } z '~/lib/sanity.client'

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

stáhnout 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: 'Metoda není povolena' })
    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)
}

i18n adaptéry

Sanity nemá předepsaný přístup k internacionalizaci a existuje mnoho způsobů, jak ji implementovat. Tento plugin používá vzor adaptéru, aby umožnil konfiguraci na základě toho, jak je obsah strukturován a jak by měl být přeložen.

V současnosti je k dispozici pouze jeden adaptér, a to documentInternationalizationAdapter, který používá oficiální document-internationalization plugin (verze ^2.0.0). Podávejte problém, pokud je vyžadován konkrétní adaptér, nebo se odkažte na tento repository's package/src/adapters/document-internationalization.ts pro příklad, jak implementovat přizpůsobený.

Vlastní transformátory dat

Pokud je potřeba transformovat data před jejich odesláním do Phrase, použijte možnost dataTransformers. To je užitečné, pokud měníte strukturu dat, nebo pokud vylučujete určité pole z překladu.

Každý datový transformátor musí kódovat data před jejich odesláním do Phrase; a dekódovat je při jejich přijímání zpět, aby je transformoval před uložením do Sanity. Více transformátorů může být navrstveno a spuštěno sekvenčně.

Plugin nenabízí žádný způsob, jak testovat transformátory izolovaně, takže vývoj může být složitý. Uložte skutečné cílové dokumenty z datasetu Sanity do .JSON a použijte je jako testovací data pro každou funkci kódování/dekódování.

Příklad úpravy JSON-kódovaných VTT souborů na HTML, aby Phrase lépe segmentoval obsah titulků:

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

const vttJsonTransformer: DataTransformer = {
  kódovat: {
    pole(arr) {
      // Zkontrolujte, zda pole obsahuje VTT uzly titulků
      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 // Vraťte undefined, abyste přeskočili transformaci
    },
  },
  dekódovat: {
    objekt(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 = {
  kódovat: {
    pole(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
    },
  },
  dekódovat: {
    objekt(obj) {
      if (!!obj && '_type' in obj && obj._type === 'encodedSubtitles') {
        return decodeSubtitles(obj as EncodedSubtitles)
      }

      return undefined
    },
  },
}

// Přeskočení implementace
// Odkaz na /demo-nextjs/src/utils/vttJsonTransformer.ts pro celý zdrojový kód
declare function decodeSubtitles(
  encoded: EncodedSubtitles,
): StoredSubtitleNode[]
declare function encodeSubtitles(nodes: StoredSubtitleNode[]): EncodedSubtitles

Omezení přístupu editorů

Kteří editoři mohou mít přístup k hlavnímu panelu Phrase, lze omezit implementací možnosti isPhraseDashboardHidden. Tato funkce je ekvivalentní té, která je předána vlastnosti hidden pole v Sanity. Přijímá kontext s aktuálním uživatelem a dokumentem a musí vrátit boolean.

Příklad omezení přístupu k hlavnímu panelu Phrase pro uživatele s rolí admin:

const PHRASE_CONFIG = definePhraseOptions({
  // ...
  isPhraseDashboardHidden: (context) => {
    const isAdmin = (context.currentUser.roles || []).some(
      (r) => r.name === 'správce',
    )
    // Skrýt, pokud nejste správce
    return !isAdmin
  },
})
Byl pro vás tento článek užitečný?

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.