Integrace

Sanity (TMS)

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

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

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 udržovány v synchronizaci, aby lingvisté a překladatelé viděli změny v náhledu v reálném čase.

  • Chytré opětovné překlady

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

  • Automatický překlad referencí

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

  • Flexibilní schémata

    Bez ohledu na strukturu se jí plugin přizpůsobí a zajistí, aby byl výsledný přeložený obsah v souladu se schématy Sanity.

  • Pracovní postupy Phrase

    Pracovní postupy překladu ve Phrase zůstávají stejné; přeškolení nebo překonfigurování operací není vyžadováno.

Instalace pluginu Sanity

Instalace se provádí na 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 ze šablon Starter poskytnutých společností Sanity.

Instalace

Přejděte do 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í plugin je nutné nastavit následující proměnné prostředí. V kořenovém adresáři projekt vytvořit soubor `.env` (nebo `.env.local` pro Next.js).

Níže uvedené příklady jsou pro NextJS. Pro ostatní frameworky se podívejte do jejich specifické dokumentace a vezměte na vědomí, že u veřejných proměnných může být nutné odstranit předponu `NEXT_PUBLIC_`.

Důležité

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

# Základní URL vašeho webu (používá se pro odkazy na náhled)
NEXT_PUBLIC_BASE_URL="http://localhost:3000"

# URL, kde bude umístěn backendový obslužný program plugin
NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT="http://localhost:3000/api/phrase"

# Oblast datového centra Phrase ('eu' nebo 'us')
NEXT_PUBLIC_PHRASE_REGION="eu"

# Konfigurace projekt Sanity
NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
NEXT_PUBLIC_SANITY_DATASET="production"

# API token Sanity s oprávněním k zápisu (pouze server)
SANITY_WRITE_TOKEN=""

# přihlašovací údaje Phrase (pouze server)
# Poznámka: API Phrase očekává pouze část uživatelského jména, NIKOLI celou e-mail 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 PHRASE_CONFIG = definePhraseOptions({
  // Vyžadováno: adaptér i18n pro internacionalizaci dokumentů
  i18nAdapter: documentInternationalizationAdapter(),

  // Vyžadováno: Typy dokumentů, které lze přeložit
  translatableTypes: ['page', 'post', 'article'],

  // Vyžadováno: Zdrojový jazyk (primární jazyk)
  // Musí odpovídat jazyku definovanému v šabloně vašeho projektu Phrase
  sourceLang: 'en',

  // Vyžadováno: Cílové jazyky, do kterých mohou uživatelé překládat
  // Použijte stejné kódy jako u vašich dokumentů Sanity
  // Tento seznam musí odpovídat jazykům definovaným v šabloně vašeho projektu Phrase
  supportedTargetLangs: ['es', 'fr', 'de', 'pt'],

  // Vyžadováno: URL koncového bodu vašeho backendového API
  apiEndpoint: process.env.NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT!,

  // Vyžadováno: Oblast datového centra Phrase ('eu' nebo 'us')
  phraseRegion: process.env.NEXT_PUBLIC_PHRASE_REGION as 'eu' | 'us',

  // Vyžadováno: Šablony projektů Phrase dostupné pro editory
  phraseTemplates: [
    {
      templateUid: 'YOUR_TEMPLATE_UID_HERE',
      label: 'Výchozí překladová šablona',
    },
  ],

  // Vyžadováno: Generovat náhledové adresy URL pro lingvisty
  getDocumentPreview: (doc, sanityClient) => {
    const publishedId = doc._id.Nahradit('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,

  // Povolit překlad konceptů dokumentů (výchozí: false)
  translateDrafts: false,

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

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

  // Skrýt hlavní panel Phrase na základě rolí uživatele
  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 PHRASE_CONFIG = definePhraseOptions({
  /**
   * Adaptér i18n, který se má pro tento plugin použít.
   * Bude zodpovědný za načítání a úpravu dokumentů pro každý cílový jazyk.
   *
   * Více informací o adaptérech naleznete níže.
   */
  i18nAdapter: documentInternationalizationAdapter(),

  /**
   * Typy schémat Sanity, které může plugin přeložit
   */
  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 jej automaticky převede 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 jej automaticky převede 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 adresa k vašemu nakonfigurovanému 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ů ve front-endu.
   */
  getDocumentPreview: async (doc, sanityClient) => {
    const publishedId = doc._id.Nahradit('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 uživatelských oprávnění.
   *
   * Přijímá kontext s aktuálním uživatelem a dokumentem a musí vrátit logickou hodnotu.
   */
  isPhraseDashboardHidden: (context) =>
    !(context.currentUser.roles || []).some((r) => r.name === 'správce'),
})

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

Konfigurace pluginu Sanity

Injekce schématu

Aby bylo možné pluginu sdělit, které typy dokumentů lze přeložit, předejte pole typů dokumentů funkci injectPhraseIntoSchema v souboru sanity.config.ts:

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

// Seznam typů schémat, které představují překládaný text. 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: {
    types: injectPhraseIntoSchema(TRANSLATABLE_SCHEMAS, PHRASE_CONFIG),
    templates: (prev) =>
      prev.filtrovat((šablona) => !TRANSLATABLE_SCHEMAS.includes(šablona.ID)),
  },
  plugins: [
    // ...
    phrasePlugin({
      // Zde jsou vaše konfigurační možnosti
    }),
  ],
})

Vyloučení PTD ze seznamů dokumentů

PTD (Phrase Translation Documents) jsou dočasné dokumenty, které by se neměly zobrazovat v běžných seznamech dokumentů v rámci Sanity Studio. Konstanta NOT_PTD poskytuje pro tento účel filtr GROQ:

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

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

Skrytí nabídky překladu z vedlejších

Při použití plugin dokument-internationalization by měla být nabídka překladu před vedlejšími skryta. Nástroj isPtdId identifikuje dokument dokument:

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

// Použít stejné pole jako translatableTypes z vašeho PHRASE_CONFIG
const TRANSLATABLE_TYPES = ['page', 'post', 'article']

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

      // Zobrazit nabídku překladu pouze pro skutečné dokumenty, nikoli pro vedlejší
      return TRANSLATABLE_TYPES.includes(schemaType) &&
        documentId &&
        !isPtdId(documentId)"}
        ? [...prev, DocumentInternationalizationMenu]
        : předchozí
    },
  },
})

Konfigurace Phrase

Plugin nemá v uživatelském rozhraní Phrase žádnou konfiguraci, ale Phrase musí být nakonfigurován tak, aby odesílal webhook oznámení na koncový bod API backendu. To umožňuje aktualizace v reálném čase v průběhu překladů.

Vytvořit webhook

Vytvořte webhook s tímto nastavením:

  • 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 je plugin informován o všech změnách v projektech Phrase a může udržovat data Sanity synchronizovaná.

Nastavení šablon projektů

Nakonfigurujte šablonu(y) projektu Phrase s vlastnostmi požadovanými pro pracovní postupy a požadavky Team. Při objednávání nového překladu lze nabídnout výběr z jedné nebo více šablon. Šablony projektů Phrase musí mít specifické nastavení importu JSON, aby plugin fungoval správně. Tato nastavení určují, která pole se odesílají překladatelům a která jsou zachována jako metadata.

Import souboru JSON

Použijte regulární výraz k vyloučení specifických klíčů:

(^|.*\/)
(_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)
/.*)

Tento výraz obsahuje duplicitní klíče záměrně, aby bylo zajištěno, že je analyzátor regulárních výrazů Phrase ignoruje. Zajistěte, aby byly správně duplikovány.

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

  • Zahrnout všechny klíče specifické pro projekt, které nevyžadují překlad, jako je slug pro obsah používající stejnou cestu napříč všemi jazyky. Nahradit YOUR_IGNORED_KEYS_HERE seznamem klíčů oddělených svislítkem, které chcete ignorovat.

  • Kontextová poznámka: /_sanityContext

Zdrojový jazyk

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

Cílové jazyky

Zajistěte, aby jazyky vybrané ve Phrase byly synchronizovány s tím, co je v konfiguraci pluginu.

Koncový bod API Sanity

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

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

Přístup ke konfiguraci obslužné rutiny pomocí vzoru Request-Response prostřednictvím import {createRequestHandler} from sanity-plugin-phrase/backend nebo použijte interní obslužnou rutinu přímo prostřednictvím import {createInternalHandler} from sanity-plugin-phrase/backend. Zajistěte, aby byly požadavky CORS zpracovány správně, studio a koncový bod mají různé původy.

Adresář aplikací NextJS není v současné době podporován, protože nesprávně analyzuje obslužnou rutinu backendu jako klientskou komponentu React.

Tento příklad ukazuje vytvoření obslužné rutiny trasy na nakonfigurované cestě apiEndpoint na /api/phrase pomocí Next.js Pages Router:

// app/api/phrase/route.ts
// Podpora API route v 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 { klient } from '~/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('přístup-Control-Allow-Origin', '*')
  res.setHeader('přístup-Control-Allow-Methods', 'POST, OPTIONS')
  res.setHeader('přístup-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.stav(405).json({ error: 'Method not allowed' })
    return
  }

  const phraseRes = await phraseHandler(
    req.method.toUpperCase() === 'POST' ? req.body : req.dotaz,
  )
  const resBody = await phraseRes.json().catch(() => {})

  Array.from(phraseRes.headers.entries()).forEach((value: [řetězec, 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'
import { writeToken } from '~/lib/sanity.api'
import { klient } 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,
})

stáhnout default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  res.setHeader('přístup-Control-Allow-Origin', '*')
  res.setHeader('přístup-Control-Allow-Methods', 'POST, OPTIONS')
  res.setHeader('přístup-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.stav(405).json({ error: 'Method not allowed' })
    return
  }

  const phraseRes = await phraseHandler(
    req.method.toUpperCase() === 'POST' ? req.body : req.dotaz,
  )
  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)
}

adaptéry i18n

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

V současné době je k dispozici pouze adaptér documentInternationalizationAdapter, který používá oficiální plugin document-internationalization od Sanity (verze ^2.0.0). Pokud je vyžadován konkrétní adaptér, nahlaste problém nebo se podívejte do tohoto úložiště package/src/adapters/document-internationalization.ts, kde najdete příklad, jak implementovat vlastní.

Vlastní transformátory dat

Pokud je vyžadována transformace dat před jejich odesláním do Phrase, použijte možnost dataTransformers. To je užitečné při změně struktury dat nebo při vyloučení určitých polí z překladu.

Každý transformátor dat musí data před odesláním do Phrase zakódovat a při jejich přijetí zpět dekódovat, aby je mohl transformovat před uložením do Sanity. Více transformátorů lze vrstvit a spouštět postupně.

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

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

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

const vttJsonTransformer: DataTransformer = {
  encode: {
    array(arr) {
      // Zkontrolovat, zda pole obsahuje uzly titulků 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 // Vrátit undefined pro přeskočení transformace
    },
  },
  decode: {
    object(obj) {
      if (!!obj && '_type' in obj && obj._type === 'encodedSubtitles') {
        return decodeSubtitles(obj as EncodedSubtitles)
      }
      return undefined
    },
  },
}

stáhnout const PHRASE_CONFIG = definePhraseOptions({
  // ...
  dataTransformers: [vttJsonTransformer],
})stáhnout 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
    },
  },
}

// Přeskakování implementace
// Viz /demo-nextjs/src/utils/vttJsonTransformer.ts pro úplný zdrojový kód
declare function decodeSubtitles(
  encoded: EncodedSubtitles,
): StoredSubtitleNode[]
declare function encodeSubtitles(nodes: StoredSubtitleNode[]): EncodedSubtitles

Omezení přístupu editora

Které editory mohou mít přístup k hlavní panel Phrase lze omezit implementací možnosti sPhraseDashboardHidden. Tato funkce je ekvivalentní funkci předané vlastnosti hidden pole v Sanity. Přijímá kontext s aktuálním uživatel a dokument a musí vrátit boolean.

Příklad omezení přístup k hlavní panel Phrase pro uživatel s role správce:

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