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 polí 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řeklad 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é znovu školit nebo konfigurovat operace.

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 na projekt obsahující instanci Sanity Studio a nainstalujte plugin:

npm install sanity-plugin-phrase

# nebo pnpm, yarn, bun

Proměnné prostředí

Před konfigurací pluginu je nutné nastavit 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 ostatní frameworky se odkazujte 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="your-project-id"
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"
PHRASE_PASSWORD="secretPassword"

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 FRAZE_CONFIG = definePhraseOptions({
  // Povinné: i18n adaptér pro mezinárodní dokumentaci
  i18nAdapter: documentInternationalizationAdapter(),

  // Povinné: Typy dokumentů, které lze překládat
  překládatelnéTypy: ['stránka', 'příspěvek', 'článek'],

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

  // Povinné: 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
  supportedTargetLangs: ['es', 'fr', 'de', 'pt'],

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

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

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

  // Povinné: 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,

  // Umožnit překlad návrhových dokumentů (výchozí: 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 === 'správce'),
})

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

const FRAZE_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 Sanity dokumentech a používán vaším front-endem. Plugin 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 Sanity dokumentech a používán vaším front-endem. Plugin automaticky přeloží do formátu Phrase.
   */
  sourceLang: 'en',

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

  /**
   * URL vašeho nakonfigurovaného backend API pluginu.
   *
   * **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',
      štítek: '[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živatelem a dokumentem a musí vrátit boolean.
   */
  isPhraseDashboardHidden: (context) =>
    !(context.currentUser.roles || []).some((r) => r.name === 'správce'),
})

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

Konfigurace pluginu Sanity

Vložení schématu

Aby bylo možné pluginu říct, které typy dokumentů mohou být překládány, předávejte pole typů dokumentů funkci injectPhraseIntoSchema v souboru sanity.config.ts:

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

// Seznam překládaných typů schémat. 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()
          .název('obsah')
          .položky([
            S.listItem()
              .název('Příspěvky')
              .schemaType('post')
              .child(
                S.documentList()
                  .název('Příspěvky')
                  .filtrovat(`_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]
        : předchozí
    },
  },
})

Konfigurace fráze

Plugin nemá 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 apiEndpoint možnost.

  • Události:

    • Zakázky

      • Zakázka odstraněna

      • Zakázka přiřazena

      • 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 Phrase a může udržovat data Sanity v synchronizaci.

Nastavení projektů šablony(š)

Konfigurujte Phrase šablonu projektu s vlastnostmi potřebný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ů Phrase musí mít specifická nastavení JSON importu, aby plugin fungoval správně. Tato nastavení řídí, které pole jsou odesílána překladatelům a která jsou uchová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 bylo zajištěno, že budou ignorovány parserem RegEx Phrase. Zajistěte, aby byly správně duplikovány.

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

  • Zahrňte 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. Nahraďte VAŠE_VYLOUČENÉ_KLÍČE_TADY seznamem klíčů oddělených svislou čarou, které mají být ignorovány.

  • Kontextová poznámka: /_sanityContext

Zdrojový jazyk

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

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 Sanity studia.

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-end frameworků jako NextJS, Remix, SvelteKit nebo Nuxt.

Přístup k nastavení 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
import type { NextApiRequest, NextApiResponse } from 'next'
import { PHRASE_CONFIG } from 'phraseConfig'
import { createInternalHandler } from 'sanity-plugin-phrase/backend'

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

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

export 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
// API cesta 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'
import { client } from '~/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,
})

export 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 jediným dostupným adaptérem documentInternationalizationAdapter, který používá oficiální document-internationalization plugin Sanity (verze ^2.0.0). Nahlaste 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ý transformátor dat musí před odesláním do Phrase data zakódovat; a dekódovat je při jejich příjmu 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 uzly titulků VTT
      if (
        arr.every(
          (položka) =>
            typeof položka === 'object' &&
            !!položka &&
            '_type' v položce &&
            typeof položka._type === 'string' &&
            položka._type.startsWith('vtt.'),
        )
      ) {
        return encodeSubtitles(arr as StoredSubtitleNode[])
      }
      vrátit undefined // Vraťte undefined, abyste přeskočili transformaci
    },
  },
  dekódovat: {
    objekt(obj) {
      pokud (!!obj && '_type' v obj && obj._type === 'encodedSubtitles') {
        vrátit decodeSubtitles(obj jako EncodedSubtitles)
      }
      vrátit 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(
          (položka) =>
            typeof položka === 'object' &&
            !!položka &&
            '_type' v položce &&
            typeof položka._type === 'string' &&
            položka._type.startsWith('vtt.'),
        )
      ) {
        return encodeSubtitles(arr as StoredSubtitleNode[])
      }

      vrátit undefined
    },
  },
  dekódovat: {
    objekt(obj) {
      pokud (!!obj && '_type' v obj && obj._type === 'encodedSubtitles') {
        vrátit decodeSubtitles(obj jako EncodedSubtitles)
      }

      vrátit undefined
    },
  },
}

// Přeskočení implementace
// Odkaz na /demo-nextjs/src/utils/vttJsonTransformer.ts pro celý zdrojový kód
deklarovat funkci decodeSubtitles(
  zakódováno: ZakódovanéTituly,
): StoredSubtitleNode[]
deklarovat funkci encodeSubtitles(nody: UloženýTitulekNode[]): ZakódovanéTituly

Omezení přístupu editorů

Kteří editoři mohou mít přístup k hlavnímu panelu Phrase, může být omezeno 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í správce:

const FRAZE_CONFIG = definePhraseOptions({
  // ...
  isPhraseDashboardHidden: (context) => {
    const isAdmin = (context.currentUser.roles || []).some(
      (r) => r.name === 'správce',
    )
    // Skrýt, pokud nejste správcem
    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.