Интеграции

Sanity (TMS)

Контент автоматически переведен с английского языка с помощью Phrase Language AI.

Плагин предоставляет доступ к переведенному контенту Phrase из студии Sanity.

Поддерживаются только переводы на уровне документа. Переводы на уровне поля не поддерживаются.

Особенности:

  • Предварительный просмотр в реальном времени

    Переводы синхронизируются, чтобы лингвисты и переводчики могли видеть изменения предварительного просмотра в реальном времени.

  • Умные повторные переводы

    Плагин сравнивает, какой контент изменился с последнего перевода, и отправляет только эти изменения в Phrase.

  • Автоматический перевод ссылок

    При выдаче переводов редакторы могут выбрать также перевести документы, на которые ссылается текущий, и плагин автоматически свяжет их по целевому языку.

  • Гибкие схемы

    Неважно, какая структура, плагин адаптируется к ней и обеспечивает, чтобы финальный переведенный контент соответствовал схемам Sanity.

  • Рабочие процессы Phrase

    Рабочие процессы перевода в Phrase остаются прежними; повторное обучение или перенастройка операций не требуется.

Установка плагина Sanity

Установка выполняется в командной строке.

Предполагается, что генератор веб-сайтов и Sanity Studio уже настроены. Если нет, используйте один из шаблонов-стартеров, предоставленных Sanity.

Установка

Перейдите к проекту, содержащему экземпляр Sanity Studio, и установите плагин:

npm install sanity-plugin-phrase

# или pnpm, yarn, bun

Переменные окружения

Перед настройкой плагина необходимо установить следующие переменные окружения. Создайте файл `.env` (или `.env.local` для Next.js) в корне проекта.

Примеры ниже приведены для NextJS. Для других фреймворков обратитесь к их документации и обратите внимание, что префикс `NEXT_PUBLIC_` может потребоваться удалить для публичных переменных.

Важно

Переменные на стороне сервера (SANITY_WRITE_TOKEN, PHRASE_USER_NAME, PHRASE_PASSWORD) никогда не должны быть доступны клиенту. В Next.js только переменные с префиксом NEXT_PUBLIC_ доступны в браузере.

# Базовый URL вашего сайта (используется для предварительных ссылок)
NEXT_PUBLIC_BASE_URL="http://localhost:3000"

# URL, где будет находиться обработчик бэкенда плагина
NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT="http://localhost:3000/api/phrase"

# Регион дата-центра Phrase ('eu' или 'us')
NEXT_PUBLIC_PHRASE_REGION="eu"

# Конфигурация проекта Sanity
NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
NEXT_PUBLIC_SANITY_DATASET="production"

# Токен API Sanity с правами на запись (только на стороне сервера)
SANITY_WRITE_TOKEN=""

# Учетные данные Phrase (только на стороне сервера)
# Примечание: API фраз ожидает только часть имени пользователя, а не полный адрес электронной почты
PHRASE_USER_NAME="phraseUsername"
PHRASE_PASSWORD="secretPassword"

Конфигурация плагина

Плагин добавляется в sanity.config.ts с необходимыми параметрами конфигурации:

// sanity.config.ts
импорт { defineConfig } из 'sanity'
импорт {
  phrasePlugin,
  definePhraseOptions,
  documentInternationalizationAdapter,
} из 'sanity-plugin-phrase'

const PHRASE_CONFIG = definePhraseOptions({
  // Обязательно: i18n адаптер для интернационализации документа
  i18nAdapter: documentInternationalizationAdapter(),

  // Обязательно: Типы документов, которые можно перевести
  translatableTypes: ['page', 'post', 'article'],

  // Обязательно: Исходный язык (основной язык)
  // Это должно соответствовать языку, определенному в шаблоне вашего проекта Phrase
  sourceLang: 'en',

  // Обязательно: Целевые языки, на которые пользователи могут переводить
  // Используйте те же коды, что и в ваших документах Sanity
  // Этот список должен совпадать с языками, определенными в шаблоне проекта Phrase
  supportedTargetLangs: ['es', 'fr', 'de', 'pt'],

  // Обязательно: URL-адрес вашего API на сервере
  apiEndpoint: process.env.NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT!,

  // Обязательно: Регион дата-центра Phrase ('eu' или 'us')
  phraseRegion: process.env.NEXT_PUBLIC_PHRASE_REGION как 'eu' | 'us',

  // Обязательно: Шаблоны проектов Phrase, доступные для редакторов
  phraseTemplates: [
    {
      templateUid: 'YOUR_TEMPLATE_UID_HERE',
      label: 'Шаблон перевода по умолчанию',
    },
  ],

  // Обязательно: Генерировать URL-адреса предварительного просмотра для лингвистов
  getDocumentPreview: (doc, sanityClient) => {
    const publishedId = doc._id.replace('drafts.', '')
    return `${process.env.NEXT_PUBLIC_BASE_URL}/api/draft?id=${publishedId}`
  },

  // Дополнительные настройки

  // Максимальная глубина для перевода ссылочных документов (по умолчанию: 3)
  maxReferencesDepth: 3,

  // Разрешить перевод черновиков документов (по умолчанию: ложь)
  translateDrafts: ложь,

  // Пользовательские преобразователи данных для специальных типов контента
  dataTransformers: [],

  // Конфигурация ведения журнала для отладки
  logger: {
    minimumLogLevel: 'info', // 'debug' | 'info' | 'warning' | 'error' | 'fatal'
  },

  // Скрыть панель управления Phrase в зависимости от ролей пользователя
  isPhraseDashboardHidden: (context) =>
    !(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})

export default defineConfig({
  // ... ваша существующая конфигурация
  plugins: [
    phrasePlugin(PHRASE_CONFIG),
    // ... другие плагины
  ],
})// sanity.config.(js|ts)
импорт {
  phrasePlugin,
  documentInternationalizationAdapter,
} из 'sanity-plugin-phrase'

const PHRASE_CONFIG = definePhraseOptions({
  /**
   * Адаптер i18n для использования с этим плагином.
   * Он будет отвечать за получение и изменение документов для каждого целевого языка.
   *
   * См. ниже для получения дополнительной информации об адаптерах.
   */
  i18nAdapter: documentInternationalizationAdapter(),

  /**
   * Типы схемы Sanity, которые плагин может переводить
   */
  translatableTypes: ['page', 'post', 'course', 'lesson', 'definition'],

  /**
   * Языковой код всех языков, на которые пользователи могут переводить.
   * Должен совпадать с тем, который хранится в ваших документах Sanity и используется вашим фронтендом. Плагин автоматически переведет его в формат Phrase.
   */
  supportedTargetLangs: ['cz', 'es', 'pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru'],

  /**
   * Языковой код исходного языка, который будет переведен.
   * Должен совпадать с тем, который хранится в ваших документах Sanity и используется вашим фронтендом. Плагин автоматически переведет его в формат Phrase.
   */
  sourceLang: 'en',

  /**
   * Как определено в настройках вашей учетной записи Phrase
   * Либо `eu`, либо `us`
   */
  phraseRegion: 'us|eu',

  /**
   * URL вашего настроенного API бэкенда плагина.
   *
   * **Примечание:** следуйте шагам для настройки конечной точки, описанным ниже
   */
  apiEndpoint: 'https://my-site.com/api/phrase',

  /**
   * Используется для перенаправления лингвистов с панели управления Phrase на предварительный просмотр их переводов на фронтенде.
   */
  getDocumentPreview: async (doc, sanityClient) => {
    const publishedId = doc._id.replace('drafts.', '')
    return `${process.env.NEXT_PUBLIC_FRONT_END_URL}/api/draft?publishedId=${publishedId}`
  },

  /**
   * Шаблоны проектов Phrase, которые ваши редакторы могут использовать при запросе переводов.
   *
   * **Примечание:** следуйте шагам для настройки шаблонов, описанным ниже
   */
  phraseTemplates: [
    {
      templateUid: '1jYg0Pc1d8kAHUyM0tgdmt',
      метка: '[Sanity.io] Шаблон по умолчанию',
    },
  ],

  /**
   * @необязательно
   * В случае, если вы хотите показать или скрыть панель управления Phrase в зависимости от привилегий пользователя.
   *
   * Получает контекст с текущим пользователем и документом и должен вернуть булево значение.
   */
  isPhraseDashboardHidden: (context) =>
    !(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})

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

Конфигурация плагина Sanity

Инъекция схемы

Чтобы указать плагину, какие типы документов могут быть переведены, передайте массив типов документов в функцию injectPhraseIntoSchema в файле sanity.config.ts:

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

// Список переводимых типов схем. Обычно экспортируется из индексного файла
// где бы вы ни находили свою схему Sanity
const TRANSLATABLE_SCHEMAS = ['страница', 'пост', 'курс', 'урок', 'определение']

export default defineConfig({
  схема: {
    типы: injectPhraseIntoSchema(TRANSLATABLE_SCHEMAS, PHRASE_CONFIG),
    шаблоны: (prev) =>
      prev.filter((template) => !TRANSLATABLE_SCHEMAS.includes(template.id)),
  },
  plugins: [
    // ...
    phrasePlugin({
      // Ваши параметры конфигурации здесь
    }),
  ],
})

Исключение PTD из списков документов

PTD (Документы перевода фраз) — это временные документы, которые не должны появляться в обычных списках документов внутри Sanity Studio. Константа NOT_PTD предоставляет фильтр GROQ для этой цели:

// sanity.config.ts
импорт { NOT_PTD } из 'sanity-plugin-phrase/utils'

export default defineConfig({
  // ... другая конфигурация
  plugins: [
    structureTool({
      структура: (S) =>
        S.list()
          .title('Контент')
          .items([
            S.listItem()
              .title('Посты')
              .schemaType('post')
              .child(
                S.documentList()
                  .title('Посты')
                  .filter(`_type == "post" && ${NOT_PTD}`),
              ),
            // ... другие элементы
          ]),
    }),
  ],
})

Скрытие меню перевода от PTD

При использовании плагина интернационализации документов меню перевода должно быть скрыто от PTD. Утилита isPtdId идентифицирует документы PTD:

импорт { isPtdId } из 'sanity-plugin-phrase/utils'
импортировать { DocumentInternationalizationMenu } из '@sanity/document-internationalization'

// Используйте тот же массив, что и translatableTypes из вашего PHRASE_CONFIG
const TRANSLATABLE_TYPES = ['страница', 'пост', 'статья']

export default defineConfig({
  документ: {
    нестабильный_languageFilter: (prev, ctx) => {
      const { schemaType, documentId } = ctx

      // Показывать меню перевода только для реальных документов, а не PTD
      return TRANSLATABLE_TYPES.includes(schemaType) &&
        documentId &&
        !isPtdId(documentId)
        ? [...prev, DocumentInternationalizationMenu]
        : prev
    },
  },
})

Конфигурация Фразы

Плагин не имеет конфигурации в интерфейсе Phrase, но Phrase должен быть настроен для отправки уведомлений вебхука на конечную точку API бэкенда. Это позволяет обновлениям в реальном времени по мере прогресса переводов.

Создать вебхук

Создайте вебхук с этими настройками:

  • URL-адрес

    URL конечной точки API плагина, как настроено в опции apiEndpoint.

  • События:

    • Задания

      • Задание удалено

      • Job assigned

      • Срок выполнения задания изменен

      • Перевод задания обновлен

    • Проекты

      • Проект удален

      • Срок выполнения проекта изменен

    • Другое

      • Предварительный перевод завершен

Это гарантирует, что плагин уведомляется о любых изменениях в проектах Phrase и может поддерживать синхронизацию данных Sanity.

Настройка проектов шаблон(ы)

Настройте Фразу шаблон проекта(ов) с необходимыми свойствами для рабочих процессов и требований команды. Можно предложить один или несколько шаблонов на выбор при заказе нового перевода. Шаблоны проектов Фразы должны иметь определенные настройки импорта JSON, чтобы плагин работал правильно. Эти настройки контролируют, какие поля отправляются переводчикам, а какие сохраняются как метаданные.

Импорт файла JSON

Используйте регулярные выражения, чтобы исключить определенные ключи:

(^|.*\/)
(_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|ВАШИ_ИСКЛЮЧЕННЫЕ_КЛЮЧИ_ЗДЕСЬ|
(_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|ВАШИ_ИСКЛЮЧЕННЫЕ_КЛЮЧИ_ЗДЕСЬ)
/.*)

Это выражение включает дублированные ключи намеренно, чтобы гарантировать, что они игнорируются парсером RegEx Фразы. Убедитесь, что они правильно дублированы.

  • Исключите данные, специфичные для локализации, такие как язык данного документа, если используется @sanity/document-internationalization.

  • Включите любые ключи, специфичные для проекта, которые не требуют перевода, такие как slug для контента, использующего один и тот же путь на всех языках. Замените ВАШИ_ИСКЛЮЧЕННЫЕ_КЛЮЧИ_ЗДЕСЬ на список ключей, разделенных символом |, которые нужно игнорировать.

  • Контекстное примечание: /_sanityContext

Язык оригинала

В настоящее время этот плагин работает на основе предположения о наличии одного языка оригинала. Шаблоны проекта(ов) должны иметь тот же язык, что и тот, который настроен в sourceLanguage плагина.

Целевые языки

Убедитесь, что выбранные языки в Phrase синхронизированы с конфигурацией плагина.

Sanity apiEndpoint

Это конечная точка, которую плагин использует для связи со студией Sanity. Она используется для аутентификации в API Phrase, получения вебхуков и запросов пользователей из студии Sanity.

Создайте пользовательскую конечную точку API в проекте Sanity для обработки этих запросов. Один из самых простых способов сделать это - использовать безсерверные функции через фронтенд-фреймворки, такие как NextJS, Remix, SvelteKit или Nuxt.

Получите доступ к конфигурации обработчика с помощью шаблона Запрос-Ответ через import {createRequestHandler} from sanity-plugin-phrase/backend или используйте внутренний обработчик напрямую через import {createInternalHandler} from sanity-plugin-phrase/backend. Убедитесь, что запросы CORS обрабатываются правильно, если студия и конечная точка имеют разные источники.

Директория приложения NextJS в настоящее время не поддерживается, так как она неправильно разбирает обработчик бэкенда как компонент клиента React.

Этот пример демонстрирует создание обработчика маршрута по настроенному пути apiEndpoint по адресу /api/phrase с использованием маршрутизатора страниц Next.js:

// app/api/phrase/route.ts
// Поддержка маршрутов API 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({
  фразаCredentials: {
    имяПользователя: process.env.PHRASE_USER_NAME || '',
    пароль: process.env.PHRASE_PASSWORD || '',
  },
  sanityClient: client.withConfig({ токен: writeToken }),
  параметрыПлагина: PHRASE_CONFIG,
})

экспортировать по умолчанию асинхронную функцию обработчика(
  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: 'Метод не разрешен' })
    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 маршрут 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({
  фразаCredentials: {
    имяПользователя: process.env.PHRASE_USER_NAME || '',
    пароль: process.env.PHRASE_PASSWORD || '',
  },
  sanityClient: client.withConfig({ токен: writeToken }),
  параметрыПлагина: PHRASE_CONFIG,
})

экспортировать по умолчанию асинхронную функцию обработчика(
  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: 'Метод не разрешен' })
    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 адаптеры

Sanity не имеет предписанного подхода к интернационализации, и существует множество способов его реализации. Этот плагин использует паттерн адаптера для настройки в зависимости от того, как структурирован контент и как его следует переводить.

В настоящее время единственный доступный адаптер - это documentInternationalizationAdapter, который используется официальным плагином документальной интернационализации (версия ^2.0.0). Подайте вопрос, если требуется конкретный адаптер, или обратитесь к этому репозиторию package/src/adapters/document-internationalization.ts для примера реализации пользовательского адаптера.

Пользовательские трансформаторы данных

Если требуется преобразование данных перед отправкой их в Phrase, используйте опцию dataTransformers. Это полезно, если необходимо изменить структуру данных или исключить определенные поля из перевода.

Каждый преобразователь данных должен закодировать данные перед отправкой их в Phrase; и декодировать их при получении обратно для преобразования перед сохранением в Sanity. Несколько преобразователей могут быть объединены и выполняться последовательно.

Плагин не предлагает способа тестирования преобразователей изолированно, поэтому разработка может быть сложной. Сохраните реальные целевые документы из набора данных Sanity в .JSON и используйте их в качестве тестовых данных для каждой функции кодирования/декодирования.

Пример изменения JSON-кодированных VTT файлов в HTML, чтобы Phrase мог лучше сегментировать контент субтитров:

импорт { DataTransformer } из 'sanity-plugin-phrase'

const vttJsonTransformer: DataTransformer = {
  encode: {
    array(arr) {
      // Проверьте, содержит ли массив узлы субтитров 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 // Вернуть undefined, чтобы пропустить преобразование
    },
  },
  декодировать: {
    объект(obj) {
      если (!!obj && '_type' в obj && obj._type === 'encodedSubtitles') {
        возврат decodeSubtitles(obj как EncodedSubtitles)
      }
      возврат неопределено
    },
  },
}

экспортировать const PHRASE_CONFIG = definePhraseOptions({
  // ...
  dataTransformers: [vttJsonTransformer],
})экспортировать 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[])
      }

      возврат неопределено
    },
  },
  декодировать: {
    объект(obj) {
      если (!!obj && '_type' в obj && obj._type === 'encodedSubtitles') {
        возврат decodeSubtitles(obj как EncodedSubtitles)
      }

      возврат неопределено
    },
  },
}

// Пропуск реализации
// Смотрите /demo-nextjs/src/utils/vttJsonTransformer.ts для полного исходного кода
объявить функцию decodeSubtitles(
  закодированный: EncodedSubtitles,
): StoredSubtitleNode[]
объявить функцию encodeSubtitles(узлы: StoredSubtitleNode[]): EncodedSubtitles

Ограничение доступа редактора

Кто может получить доступ к панели управления Phrase, можно ограничить, реализовав опцию isPhraseDashboardHidden. Эта функция эквивалентна той, что передана свойству hidden поля в Sanity. Он получает контекст с текущим пользователем и документом и должен вернуть логическое значение.

Пример ограничения доступа к панели управления Phrase для пользователей с ролью администратора:

const PHRASE_CONFIG = definePhraseOptions({
  // ...
  isPhraseDashboardHidden: (контекст) => {
    const isAdmin = (контекст.currentUser.roles || []).some(
      (r) => r.name === 'администратор',
    )
    // Скрыть, если не администратор
    return !isAdmin
  },
})
Была ли эта статья полезной?

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.