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 Phrase přeložený obsah přímo ze Sanity studia.

Podporovány jsou pouze překlady na úrovni dokument. Překlady na úrovni polí nejsou podporovány.

Funkce:

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

    Překlady jsou udržovány v synchronizaci, aby lingvisté a překladatelé mohli vidět změny v náhled 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ý jazyk.

  • Flexibilní schémata

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

  • Phrase pracovní postupy

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

Instalace plugin 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 z Starter šablon 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 Další.js).

Níže uvedené příklady jsou pro DalšíJS. 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é jméno, NIKOLI celou e-mail adresu
PHRASE_USER_NAME="phraseUsername"
PHRASE_PASSWORD="secretPassword"

Konfigurace plugin

Tento 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 internacionalizaci dokumentů
  i18nAdapter: documentInternationalizationAdapter(),

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

  // Povinné: Zdroj jazyk (primární jazyk)
  // Toto musí odpovídat jazyk definovanému ve vaší šablona projektu Phrase
  sourceLang: 'en',

  // Povinné: Cíl jazyky, do kterých mohou uživatelé překládat
  // Použít stejné kódy jako vaše dokumenty Sanity
  // Tento seznam musí odpovídat jazyky definovaným ve vaší šablona projektu Phrase
  supportedTargetLangs: ['es', 'fr', 'de', 'pt'],

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

  // Povinné: Oblast datového centra Phrase ('eu' nebo 'us')
  phraseRegion: process.env.NEXT_PUBLIC_PHRASE_REGION as 'eu' | 'us',

  // Povinné: Šablony projekt Phrase dostupné pro editory
  phraseTemplates: [
    {
      templateUid: 'YOUR_TEMPLATE_UID_HERE',
      label: 'Výchozí šablona překladu',
    },
  ],

  // Povinné: Generovat adresy URL náhledů 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ž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 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, které jsou uvedeny 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, které jsou uvedeny 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é jsou překládaný text. Obvykle exportováno z indexového souboru
// kdekoli máte umístěno své Sanity Schema
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 seznamy dokumentů uvnitř Sanity Studio. Konstanta NOT_PTD poskytuje pro tento účel GROQ filtrovat:

// 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}`),
              ),
            // ... ostatní položky
          ]),
    }),
  ],
})

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

Při použití pluginu document-internationalization by měla být nabídka překladu pro vedlejší dokumenty skryta. Nástroj isPtdId identifikuje dokumenty vedlejší:

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ší dokumenty
      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řit webhook s tímto nastavení:

  • URL

    Adresa URL ke koncovému 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 správně fungoval. Tato nastavení určují, která pole jsou odesílána překladatelům a která jsou zachována jako metadata.

import souboru JSON

Použít regulární výraz k Vyloučit specifické klíče:

(^|.*\/)
(_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 zvolené ve Phrase byly synchronizovány s tím, co je v konfiguraci pluginu.

Sanity API endpoint

Toto je endpoint, 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í API endpoint 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 handleru s modelem Request-Response prostřednictvím import {createRequestHandler} from sanity-plugin-phrase/backend nebo použít interní handler 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 endpoint mají různé původy.

Adresář app v NextJS není v současné době podporován, protože nesprávně analyzuje backend handler jako klientskou komponentu React.

Tento příklad ukazuje vytvoření Route Handleru na nakonfigurované cestě apiEndpoint na /api/phrase pomocí Next.js Pages Router:

// app/api/phrase/route.ts
// Další.js API route support: 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.klient'

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.klient'

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)
}

i18n adaptéry

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ý je používán oficiálním document-internationalization plugin od Sanity (verze ^2.0.0). Pokud je vyžadován konkrétní adaptér, založte problém, nebo se podívejte do tohoto repository's package/src/adapters/document-internationalization.ts pro 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. Toto 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 zakódovat před odesláním do Phrase; a dekódovat je při jejich přijetí zpět, aby je transformoval před uložením do Sanity. Více transformátorů lze vrstvit a spouštět postupně.

Tento 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 kódování/dekódování.

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 === 'řetězec' &&
            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
    },
  },
}

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 === 'řetězec' &&
            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řístup editoru

Které editor může mít přístup k hlavní panel Phrase lze omezit implementací možnosti isPhraseDashboardHidden. Tato funkce je ekvivalentní funkci předané vlastnosti hidden pole v Sanity. Přijímá kontext s aktuální 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.